mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 19:21:50 +00:00
Use comlink for extension host instead of JSON RPC 2 (#2344)
This commit is contained in:
parent
330285b2c8
commit
ebc8a9a245
@ -3,8 +3,10 @@
|
||||
import '../../config/polyfill'
|
||||
|
||||
import { without } from 'lodash'
|
||||
import { noop } from 'rxjs'
|
||||
import { fromEventPattern, noop, Observable } from 'rxjs'
|
||||
import { bufferCount, filter, groupBy, map, mergeMap } from 'rxjs/operators'
|
||||
import DPT from 'webext-domain-permission-toggle'
|
||||
import { createExtensionHostWorker } from '../../../../../shared/src/api/extension/worker'
|
||||
import * as browserAction from '../../browser/browserAction'
|
||||
import * as omnibox from '../../browser/omnibox'
|
||||
import * as permissions from '../../browser/permissions'
|
||||
@ -14,7 +16,7 @@ import * as tabs from '../../browser/tabs'
|
||||
import { featureFlagDefaults, FeatureFlags } from '../../browser/types'
|
||||
import initializeCli from '../../libs/cli'
|
||||
import { initSentry } from '../../libs/sentry'
|
||||
import { createBlobURLForBundle, createExtensionHostWorker } from '../../platform/worker'
|
||||
import { createBlobURLForBundle } from '../../platform/worker'
|
||||
import { requestGraphQL } from '../../shared/backend/graphql'
|
||||
import { resolveClientConfiguration } from '../../shared/backend/server'
|
||||
import { DEFAULT_SOURCEGRAPH_URL, setSourcegraphUrl } from '../../shared/util/context'
|
||||
@ -366,17 +368,74 @@ function setDefaultBrowserAction(): void {
|
||||
browserAction.onClicked(noop)
|
||||
setDefaultBrowserAction()
|
||||
|
||||
// This is the entrypoint for the extension host.
|
||||
chrome.runtime.onConnect.addListener(port => {
|
||||
const worker = createExtensionHostWorker()
|
||||
worker.addEventListener('message', m => {
|
||||
port.postMessage(m.data)
|
||||
})
|
||||
port.onMessage.addListener(m => {
|
||||
worker.postMessage(m)
|
||||
})
|
||||
port.onDisconnect.addListener(() => worker.terminate())
|
||||
})
|
||||
|
||||
// Add "Enable Sourcegraph on this domain" context menu item
|
||||
DPT.addContextMenu()
|
||||
|
||||
const ENDPOINT_KIND_REGEX = /^(proxy|expose)-/
|
||||
|
||||
const portKind = (port: chrome.runtime.Port) => {
|
||||
const match = port.name.match(ENDPOINT_KIND_REGEX)
|
||||
return match && match[1]
|
||||
}
|
||||
|
||||
/**
|
||||
* A stream of EndpointPair created from Port objects emitted by chrome.runtime.onConnect.
|
||||
*
|
||||
* Each EndpointPair represents a connection with an instance of the content script.
|
||||
*/
|
||||
const endpointPairs: Observable<{ proxy: chrome.runtime.Port; expose: chrome.runtime.Port }> = fromEventPattern<
|
||||
chrome.runtime.Port
|
||||
>(
|
||||
handler => chrome.runtime.onConnect.addListener(handler),
|
||||
handler => chrome.runtime.onConnect.removeListener(handler)
|
||||
).pipe(
|
||||
groupBy(
|
||||
port => (port.name || 'other').replace(ENDPOINT_KIND_REGEX, ''),
|
||||
port => port,
|
||||
group => group.pipe(bufferCount(2))
|
||||
),
|
||||
filter(group => group.key !== 'other'),
|
||||
mergeMap(group =>
|
||||
group.pipe(
|
||||
bufferCount(2),
|
||||
map(ports => {
|
||||
const proxyPort = ports.find(port => portKind(port) === 'proxy')
|
||||
if (!proxyPort) {
|
||||
throw new Error('No proxy port')
|
||||
}
|
||||
const exposePort = ports.find(port => portKind(port) === 'expose')
|
||||
if (!exposePort) {
|
||||
throw new Error('No expose port')
|
||||
}
|
||||
return {
|
||||
proxy: proxyPort,
|
||||
expose: exposePort,
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Create one extension host worker per endpoint pair
|
||||
endpointPairs.subscribe(({ proxy, expose }) => {
|
||||
const { worker, clientEndpoints } = createExtensionHostWorker({ wrapEndpoints: true })
|
||||
// Connect proxy client endpoint
|
||||
clientEndpoints.proxy.start()
|
||||
proxy.onMessage.addListener(message => {
|
||||
clientEndpoints.proxy.postMessage(message)
|
||||
})
|
||||
clientEndpoints.proxy.addEventListener('message', ({ data }) => {
|
||||
proxy.postMessage(data)
|
||||
})
|
||||
// Connect expose client endpoint
|
||||
clientEndpoints.expose.start()
|
||||
expose.onMessage.addListener(message => {
|
||||
clientEndpoints.expose.postMessage(message)
|
||||
})
|
||||
clientEndpoints.expose.addEventListener('message', ({ data }) => {
|
||||
expose.postMessage(data)
|
||||
})
|
||||
// Kill worker when either port disconnects
|
||||
proxy.onDisconnect.addListener(() => worker.terminate())
|
||||
expose.onDisconnect.addListener(() => worker.terminate())
|
||||
})
|
||||
|
||||
@ -5,7 +5,6 @@ import * as GQL from '../../../../shared/src/graphql/schema'
|
||||
import { PlatformContext } from '../../../../shared/src/platform/context'
|
||||
import { mutateSettings, updateSettings } from '../../../../shared/src/settings/edit'
|
||||
import { EMPTY_SETTINGS_CASCADE, gqlToCascade } from '../../../../shared/src/settings/settings'
|
||||
import { LocalStorageSubject } from '../../../../shared/src/util/LocalStorageSubject'
|
||||
import { toPrettyBlobURL } from '../../../../shared/src/util/url'
|
||||
import { ExtensionStorageSubject } from '../browser/ExtensionStorageSubject'
|
||||
import * as runtime from '../browser/runtime'
|
||||
@ -141,7 +140,6 @@ export function createPlatformContext({ urlToFile }: Pick<CodeHost, 'urlToFile'>
|
||||
},
|
||||
sourcegraphURL: sourcegraphUrl,
|
||||
clientApplication: 'other',
|
||||
traceExtensionHostCommunication: new LocalStorageSubject<boolean>('traceExtensionHostCommunication', false),
|
||||
sideloadedExtensionURL: new ExtensionStorageSubject('sideloadedExtensionURL', null),
|
||||
}
|
||||
return context
|
||||
|
||||
@ -1,103 +1,57 @@
|
||||
import { Observable, of } from 'rxjs'
|
||||
import * as MessageChannelAdapter from '@sourcegraph/comlink/messagechanneladapter'
|
||||
import { Observable } from 'rxjs'
|
||||
import uuid from 'uuid'
|
||||
import { MessageTransports } from '../../../../shared/src/api/protocol/jsonrpc2/connection'
|
||||
import { Message } from '../../../../shared/src/api/protocol/jsonrpc2/messages'
|
||||
import {
|
||||
AbstractMessageReader,
|
||||
AbstractMessageWriter,
|
||||
DataCallback,
|
||||
MessageReader,
|
||||
MessageWriter,
|
||||
} from '../../../../shared/src/api/protocol/jsonrpc2/transport'
|
||||
import { createWebWorkerMessageTransports } from '../../../../shared/src/api/protocol/jsonrpc2/transports/webWorker'
|
||||
import { createExtensionHost as createInPageExtensionHost } from '../../../../shared/src/api/extension/worker'
|
||||
import { EndpointPair } from '../../../../shared/src/platform/context'
|
||||
import { isInPage } from '../context'
|
||||
import { createExtensionHostWorker } from './worker'
|
||||
|
||||
/**
|
||||
* Spawns an extension and returns a communication channel to it.
|
||||
* Returns an observable of a communication channel to an extension host.
|
||||
*/
|
||||
export function createExtensionHost(): Observable<MessageTransports> {
|
||||
export function createExtensionHost(): Observable<EndpointPair> {
|
||||
if (isInPage) {
|
||||
return createInPageExtensionHost()
|
||||
return createInPageExtensionHost({ wrapEndpoints: false })
|
||||
}
|
||||
const channelID = uuid.v4()
|
||||
return of(createPortMessageTransports(chrome.runtime.connect({ name: channelID })))
|
||||
}
|
||||
|
||||
function createInPageExtensionHost(): Observable<MessageTransports> {
|
||||
const worker = createExtensionHostWorker()
|
||||
const messageTransports = createWebWorkerMessageTransports(worker)
|
||||
return new Observable(sub => {
|
||||
sub.next(messageTransports)
|
||||
return () => worker.terminate()
|
||||
const id = uuid.v4()
|
||||
return new Observable(subscriber => {
|
||||
const proxyPort = chrome.runtime.connect({ name: `proxy-${id}` })
|
||||
const exposePort = chrome.runtime.connect({ name: `expose-${id}` })
|
||||
subscriber.next({
|
||||
proxy: endpointFromPort(proxyPort),
|
||||
expose: endpointFromPort(exposePort),
|
||||
})
|
||||
return () => {
|
||||
proxyPort.disconnect()
|
||||
exposePort.disconnect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
class PortMessageReader extends AbstractMessageReader implements MessageReader {
|
||||
private pending: Message[] = []
|
||||
private callback: DataCallback | null = null
|
||||
|
||||
constructor(private port: chrome.runtime.Port) {
|
||||
super()
|
||||
|
||||
port.onMessage.addListener((message: any) => {
|
||||
try {
|
||||
if (this.callback) {
|
||||
this.callback(message)
|
||||
} else {
|
||||
this.pending.push(message)
|
||||
}
|
||||
} catch (err) {
|
||||
this.fireError(err)
|
||||
function endpointFromPort(port: chrome.runtime.Port): MessagePort {
|
||||
const listeners = new Map<(event: MessageEvent) => any, (message: object, port: chrome.runtime.Port) => void>()
|
||||
return MessageChannelAdapter.wrap({
|
||||
send(data): void {
|
||||
port.postMessage(data)
|
||||
},
|
||||
addEventListener(event, messageListener): void {
|
||||
if (event !== 'message') {
|
||||
return
|
||||
}
|
||||
})
|
||||
port.onDisconnect.addListener(() => {
|
||||
this.fireClose()
|
||||
})
|
||||
}
|
||||
|
||||
public listen(callback: DataCallback): void {
|
||||
if (this.callback) {
|
||||
throw new Error('callback is already set')
|
||||
}
|
||||
this.callback = callback
|
||||
while (this.pending.length !== 0) {
|
||||
callback(this.pending.pop()!)
|
||||
}
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
super.unsubscribe()
|
||||
this.callback = null
|
||||
this.port.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
class PortMessageWriter extends AbstractMessageWriter implements MessageWriter {
|
||||
private errorCount = 0
|
||||
|
||||
constructor(private port: chrome.runtime.Port) {
|
||||
super()
|
||||
}
|
||||
|
||||
public write(message: Message): void {
|
||||
try {
|
||||
this.port.postMessage(message)
|
||||
} catch (error) {
|
||||
this.fireError(error, message, ++this.errorCount)
|
||||
}
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
super.unsubscribe()
|
||||
this.port.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates JSON-RPC2 message transports for the Web Worker message communication interface. */
|
||||
function createPortMessageTransports(port: chrome.runtime.Port): MessageTransports {
|
||||
return {
|
||||
reader: new PortMessageReader(port),
|
||||
writer: new PortMessageWriter(port),
|
||||
}
|
||||
const chromePortListener = (data: object) => {
|
||||
messageListener.call(this, new MessageEvent('message', { data }))
|
||||
}
|
||||
listeners.set(messageListener, chromePortListener)
|
||||
port.onMessage.addListener(chromePortListener)
|
||||
},
|
||||
removeEventListener(event, messageListener): void {
|
||||
if (event !== 'message') {
|
||||
return
|
||||
}
|
||||
const chromePortListener = listeners.get(messageListener)
|
||||
if (!chromePortListener) {
|
||||
return
|
||||
}
|
||||
port.onMessage.removeListener(chromePortListener)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,9 +1,4 @@
|
||||
import { ajax } from 'rxjs/ajax'
|
||||
import ExtensionHostWorker from 'worker-loader?inline!../../../../shared/src/api/extension/main.worker.ts'
|
||||
|
||||
export function createExtensionHostWorker(): ExtensionHostWorker {
|
||||
return new ExtensionHostWorker()
|
||||
}
|
||||
|
||||
export async function createBlobURLForBundle(bundleURL: string): Promise<string> {
|
||||
const req = await ajax({
|
||||
|
||||
@ -15,7 +15,7 @@ const config = {
|
||||
// https://github.com/facebook/create-react-app/issues/5241#issuecomment-426269242 for more information on why
|
||||
// this is necessary.
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!abortable-rx|@sourcegraph/react-loading-spinner|@sourcegraph/codeintellify)',
|
||||
'/node_modules/(?!abortable-rx|@sourcegraph/react-loading-spinner|@sourcegraph/codeintellify|@sourcegraph/comlink)',
|
||||
],
|
||||
|
||||
moduleNameMapper: { '\\.s?css$': 'identity-obj-proxy' },
|
||||
|
||||
@ -139,6 +139,7 @@
|
||||
"jest": "^23.6.0",
|
||||
"json-schema-ref-parser": "^6.0.2",
|
||||
"json-schema-to-typescript": "^6.1.0",
|
||||
"message-port-polyfill": "^0.1.0",
|
||||
"mini-css-extract-plugin": "^0.5.0",
|
||||
"mkdirp-promise": "^5.0.1",
|
||||
"mz": "^2.7.0",
|
||||
@ -181,6 +182,7 @@
|
||||
"@sentry/browser": "^4.5.4",
|
||||
"@slimsag/react-shortcuts": "^1.2.1",
|
||||
"@sourcegraph/codeintellify": "^6.0.3",
|
||||
"@sourcegraph/comlink": "^3.1.1-fork.3",
|
||||
"@sourcegraph/extension-api-types": "link:packages/@sourcegraph/extension-api-types",
|
||||
"@sourcegraph/react-loading-spinner": "0.0.7",
|
||||
"@sqs/jsonc-parser": "^1.0.3",
|
||||
|
||||
@ -1211,10 +1211,14 @@ declare module 'sourcegraph' {
|
||||
/** @deprecated Use an observer instead of a complete callback */
|
||||
subscribe(next: null | undefined, error: null | undefined, complete: () => void): Unsubscribable
|
||||
/** @deprecated Use an observer instead of an error callback */
|
||||
subscribe(next: null | undefined, error: (error: any) => void, complete?: () => void): Unsubscribable
|
||||
subscribe(next: null | undefined, error: (error: any) => void, complete?: (() => void) | null): Unsubscribable
|
||||
/** @deprecated Use an observer instead of a complete callback */
|
||||
subscribe(next: (value: T) => void, error: null | undefined, complete: () => void): Unsubscribable
|
||||
subscribe(next?: (value: T) => void, error?: (error: any) => void, complete?: () => void): Unsubscribable
|
||||
subscribe(
|
||||
next?: ((value: T) => void) | null,
|
||||
error?: ((error: any) => void) | null,
|
||||
complete?: (() => void) | null
|
||||
): Unsubscribable
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
24
shared/src/api/client/api/api.ts
Normal file
24
shared/src/api/client/api/api.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { ClientCodeEditorAPI } from './codeEditor'
|
||||
import { ClientCommandsAPI } from './commands'
|
||||
import { ClientConfigurationAPI } from './configuration'
|
||||
import { ClientContextAPI } from './context'
|
||||
import { ClientLanguageFeaturesAPI } from './languageFeatures'
|
||||
import { ClientSearchAPI } from './search'
|
||||
import { ClientViewsAPI } from './views'
|
||||
import { ClientWindowsAPI } from './windows'
|
||||
|
||||
/**
|
||||
* The API that is exposed from the client (main thread) to the extension host (worker)
|
||||
*/
|
||||
export interface ClientAPI {
|
||||
ping(): 'pong'
|
||||
|
||||
context: ClientContextAPI
|
||||
configuration: ClientConfigurationAPI
|
||||
search: ClientSearchAPI
|
||||
languageFeatures: ClientLanguageFeaturesAPI
|
||||
commands: ClientCommandsAPI
|
||||
windows: ClientWindowsAPI
|
||||
codeEditor: ClientCodeEditorAPI
|
||||
views: ClientViewsAPI
|
||||
}
|
||||
@ -1,14 +1,13 @@
|
||||
import { ProxyValue, proxyValueSymbol } from '@sourcegraph/comlink'
|
||||
import { TextDocumentDecoration } from '@sourcegraph/extension-api-types'
|
||||
import { flatten, values } from 'lodash'
|
||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs'
|
||||
import { handleRequests } from '../../common/proxy'
|
||||
import { Connection } from '../../protocol/jsonrpc2/connection'
|
||||
import { ProvideTextDocumentDecorationSignature } from '../services/decoration'
|
||||
import { FeatureProviderRegistry } from '../services/registry'
|
||||
import { TextDocumentIdentifier } from '../types/textDocument'
|
||||
|
||||
/** @internal */
|
||||
export interface ClientCodeEditorAPI {
|
||||
export interface ClientCodeEditorAPI extends ProxyValue {
|
||||
$setDecorations(resource: string, decorationType: string, decorations: TextDocumentDecoration[]): void
|
||||
}
|
||||
|
||||
@ -20,6 +19,8 @@ interface PreviousDecorations {
|
||||
|
||||
/** @internal */
|
||||
export class ClientCodeEditor implements ClientCodeEditorAPI {
|
||||
public readonly [proxyValueSymbol] = true
|
||||
|
||||
private subscriptions = new Subscription()
|
||||
|
||||
/** Map of document URI to its decorations (last published by the server). */
|
||||
@ -27,10 +28,7 @@ export class ClientCodeEditor implements ClientCodeEditorAPI {
|
||||
|
||||
private previousDecorations: PreviousDecorations = {}
|
||||
|
||||
constructor(
|
||||
connection: Connection,
|
||||
private registry: FeatureProviderRegistry<undefined, ProvideTextDocumentDecorationSignature>
|
||||
) {
|
||||
constructor(private registry: FeatureProviderRegistry<undefined, ProvideTextDocumentDecorationSignature>) {
|
||||
this.subscriptions.add(
|
||||
this.registry.registerProvider(
|
||||
undefined,
|
||||
@ -38,8 +36,6 @@ export class ClientCodeEditor implements ClientCodeEditorAPI {
|
||||
this.getDecorationsSubject(textDocument.uri)
|
||||
)
|
||||
)
|
||||
|
||||
handleRequests(connection, 'codeEditor', this)
|
||||
}
|
||||
|
||||
public $setDecorations(resource: string, decorationType: string, decorations: TextDocumentDecoration[]): void {
|
||||
|
||||
@ -1,48 +1,24 @@
|
||||
import { Subscription } from 'rxjs'
|
||||
import { createProxyAndHandleRequests } from '../../common/proxy'
|
||||
import { ExtCommandsAPI } from '../../extension/api/commands'
|
||||
import { Connection } from '../../protocol/jsonrpc2/connection'
|
||||
import { ProxyValue, proxyValue, proxyValueSymbol } from '@sourcegraph/comlink'
|
||||
import { Unsubscribable } from 'sourcegraph'
|
||||
import { CommandRegistry } from '../services/command'
|
||||
import { SubscriptionMap } from './common'
|
||||
|
||||
/** @internal */
|
||||
export interface ClientCommandsAPI {
|
||||
$unregister(id: number): void
|
||||
$registerCommand(id: number, command: string): void
|
||||
export interface ClientCommandsAPI extends ProxyValue {
|
||||
$registerCommand(name: string, command: (...args: any) => any): Unsubscribable & ProxyValue
|
||||
$executeCommand(command: string, args: any[]): Promise<any>
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ClientCommands implements ClientCommandsAPI {
|
||||
private subscriptions = new Subscription()
|
||||
private registrations = new SubscriptionMap()
|
||||
private proxy: ExtCommandsAPI
|
||||
export class ClientCommands implements ClientCommandsAPI, ProxyValue {
|
||||
public readonly [proxyValueSymbol] = true
|
||||
|
||||
constructor(connection: Connection, private registry: CommandRegistry) {
|
||||
this.subscriptions.add(this.registrations)
|
||||
constructor(private registry: CommandRegistry) {}
|
||||
|
||||
this.proxy = createProxyAndHandleRequests('commands', connection, this)
|
||||
}
|
||||
|
||||
public $unregister(id: number): void {
|
||||
this.registrations.remove(id)
|
||||
}
|
||||
|
||||
public $registerCommand(id: number, command: string): void {
|
||||
this.registrations.add(
|
||||
id,
|
||||
this.registry.registerCommand({
|
||||
command,
|
||||
run: (...args: any[]): any => this.proxy.$executeCommand(id, args),
|
||||
})
|
||||
)
|
||||
public $registerCommand(command: string, run: (...args: any) => any): Unsubscribable & ProxyValue {
|
||||
return proxyValue(this.registry.registerCommand({ command, run }))
|
||||
}
|
||||
|
||||
public $executeCommand(command: string, args: any[]): Promise<any> {
|
||||
return this.registry.executeCommand({ command, arguments: args })
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
this.subscriptions.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,53 +1,51 @@
|
||||
import { Unsubscribable } from 'sourcegraph'
|
||||
import { ProxyResult, proxyValueSymbol } from '@sourcegraph/comlink'
|
||||
import { noop } from 'lodash'
|
||||
import { from, Observable, observable, Subscription } from 'rxjs'
|
||||
import { mergeMap } from 'rxjs/operators'
|
||||
import { Subscribable } from 'sourcegraph'
|
||||
import { ProxySubscribable } from '../../extension/api/common'
|
||||
import { syncSubscription } from '../../util'
|
||||
|
||||
/**
|
||||
* Manages a map of subscriptions keyed on a numeric ID.
|
||||
* When a Subscribable is returned from the other thread (wrapped with `proxySubscribable()`),
|
||||
* this thread gets a `Promise` for a `Subscribable` _proxy_ where `subscribe()` returns a `Promise<Unsubscribable>`.
|
||||
* This function wraps that proxy in a real Rx Observable where `subscribe()` returns an `Unsubscribable` directly as expected.
|
||||
*
|
||||
* @internal
|
||||
* @param proxyPromise The proxy to the `ProxyObservable` in the other thread
|
||||
*/
|
||||
export class SubscriptionMap {
|
||||
private map = new Map<number, Unsubscribable>()
|
||||
|
||||
/**
|
||||
* Adds a new subscription with the given {@link id}.
|
||||
*
|
||||
* @param id - A unique identifier for this subscription among all other entries in this map.
|
||||
* @param subscription - The subscription, unsubscribed when {@link SubscriptionMap#remove} is called.
|
||||
* @throws If there already exists an entry with the given {@link id}.
|
||||
*/
|
||||
public add(id: number, subscription: Unsubscribable): void {
|
||||
if (this.map.has(id)) {
|
||||
throw new Error(`subscription already exists with ID ${id}`)
|
||||
}
|
||||
this.map.set(id, subscription)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes the subscription that was previously added with the given {@link id}, and removes it from the
|
||||
* map.
|
||||
*/
|
||||
public remove(id: number): void {
|
||||
const subscription = this.map.get(id)
|
||||
if (!subscription) {
|
||||
throw new Error(`no subscription with ID ${id}`)
|
||||
}
|
||||
try {
|
||||
subscription.unsubscribe()
|
||||
} finally {
|
||||
this.map.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes all subscriptions in this map and clears it.
|
||||
*/
|
||||
public unsubscribe(): void {
|
||||
try {
|
||||
for (const subscription of this.map.values()) {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
} finally {
|
||||
this.map.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
export const wrapRemoteObservable = <T>(proxyPromise: Promise<ProxyResult<ProxySubscribable<T>>>): Observable<T> =>
|
||||
from(proxyPromise).pipe(
|
||||
mergeMap(
|
||||
proxySubscribable =>
|
||||
// tslint:disable-next-line: no-object-literal-type-assertion
|
||||
({
|
||||
// Needed for Rx type check
|
||||
[observable](): Subscribable<T> {
|
||||
return this
|
||||
},
|
||||
subscribe(...args: any[]): Subscription {
|
||||
// Always subscribe with an object because the other side
|
||||
// is unable to tell if a Proxy is a function or an observer object
|
||||
// (they always appear as functions)
|
||||
let proxyObserver: Parameters<(typeof proxySubscribable)['subscribe']>[0]
|
||||
if (typeof args[0] === 'function') {
|
||||
proxyObserver = {
|
||||
[proxyValueSymbol]: true,
|
||||
next: args[0] || noop,
|
||||
error: args[1] || noop,
|
||||
complete: args[2] || noop,
|
||||
}
|
||||
} else {
|
||||
const partialObserver = args[0] || {}
|
||||
proxyObserver = {
|
||||
[proxyValueSymbol]: true,
|
||||
next: partialObserver.next ? val => partialObserver.next(val) : noop,
|
||||
error: partialObserver.error ? err => partialObserver.error(err) : noop,
|
||||
complete: partialObserver.complete ? () => partialObserver.complete() : noop,
|
||||
}
|
||||
}
|
||||
return syncSubscription(proxySubscribable.subscribe(proxyObserver))
|
||||
},
|
||||
} as Subscribable<T>)
|
||||
)
|
||||
)
|
||||
|
||||
@ -1,27 +1,25 @@
|
||||
import { ProxyResult, ProxyValue, proxyValueSymbol } from '@sourcegraph/comlink'
|
||||
import { from, Subscription } from 'rxjs'
|
||||
import { switchMap } from 'rxjs/operators'
|
||||
import { isSettingsValid } from '../../../settings/settings'
|
||||
import { createProxyAndHandleRequests } from '../../common/proxy'
|
||||
import { ExtConfigurationAPI } from '../../extension/api/configuration'
|
||||
import { Connection } from '../../protocol/jsonrpc2/connection'
|
||||
import { SettingsEdit, SettingsService } from '../services/settings'
|
||||
|
||||
/** @internal */
|
||||
export interface ClientConfigurationAPI {
|
||||
$acceptConfigurationUpdate(edit: SettingsEdit): Promise<void>
|
||||
export interface ClientConfigurationAPI extends ProxyValue {
|
||||
$acceptConfigurationUpdate(edit: SettingsEdit): void
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @template C - The configuration schema.
|
||||
*/
|
||||
export class ClientConfiguration<C> implements ClientConfigurationAPI {
|
||||
export class ClientConfiguration<C> implements ClientConfigurationAPI, ProxyValue {
|
||||
public readonly [proxyValueSymbol] = true
|
||||
|
||||
private subscriptions = new Subscription()
|
||||
private proxy: ExtConfigurationAPI<C>
|
||||
|
||||
constructor(connection: Connection, private settingsService: SettingsService<C>) {
|
||||
this.proxy = createProxyAndHandleRequests('configuration', connection, this)
|
||||
|
||||
constructor(private proxy: ProxyResult<ExtConfigurationAPI<C>>, private settingsService: SettingsService<C>) {
|
||||
this.subscriptions.add(
|
||||
from(settingsService.data)
|
||||
.pipe(
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
import { Subscription } from 'rxjs'
|
||||
import { ContextValues } from 'sourcegraph'
|
||||
import { handleRequests } from '../../common/proxy'
|
||||
import { Connection } from '../../protocol/jsonrpc2/connection'
|
||||
import { ProxyValue, proxyValueSymbol } from '@sourcegraph/comlink'
|
||||
import { ContextValues, Unsubscribable } from 'sourcegraph'
|
||||
|
||||
/** @internal */
|
||||
export interface ClientContextAPI {
|
||||
export interface ClientContextAPI extends ProxyValue {
|
||||
$acceptContextUpdates(updates: ContextValues): void
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ClientContext implements ClientContextAPI {
|
||||
private subscriptions = new Subscription()
|
||||
export class ClientContext implements ClientContextAPI, Unsubscribable {
|
||||
public readonly [proxyValueSymbol] = true
|
||||
|
||||
/**
|
||||
* Context keys set by this server. To ensure that context values are cleaned up, all context properties that
|
||||
@ -20,9 +18,7 @@ export class ClientContext implements ClientContextAPI {
|
||||
*/
|
||||
private keys = new Set<string>()
|
||||
|
||||
constructor(connection: Connection, private updateContext: (updates: ContextValues) => void) {
|
||||
handleRequests(connection, 'context', this)
|
||||
}
|
||||
constructor(private updateContext: (updates: ContextValues) => void) {}
|
||||
|
||||
public $acceptContextUpdates(updates: ContextValues): void {
|
||||
for (const key of Object.keys(updates)) {
|
||||
@ -41,7 +37,5 @@ export class ClientContext implements ClientContextAPI {
|
||||
}
|
||||
this.keys.clear()
|
||||
this.updateContext(updates)
|
||||
|
||||
this.subscriptions.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,26 +1,22 @@
|
||||
import { ProxyResult } from '@sourcegraph/comlink'
|
||||
import { Observable, Subscription } from 'rxjs'
|
||||
import { createProxyAndHandleRequests } from '../../common/proxy'
|
||||
import { ExtDocumentsAPI } from '../../extension/api/documents'
|
||||
import { Connection } from '../../protocol/jsonrpc2/connection'
|
||||
import { TextDocumentItem } from '../types/textDocument'
|
||||
import { SubscriptionMap } from './common'
|
||||
|
||||
/** @internal */
|
||||
export class ClientDocuments {
|
||||
private subscriptions = new Subscription()
|
||||
private registrations = new SubscriptionMap()
|
||||
private proxy: ExtDocumentsAPI
|
||||
|
||||
constructor(connection: Connection, modelTextDocuments: Observable<TextDocumentItem[] | null>) {
|
||||
this.proxy = createProxyAndHandleRequests('documents', connection, this)
|
||||
|
||||
constructor(
|
||||
private proxy: ProxyResult<ExtDocumentsAPI>,
|
||||
modelTextDocuments: Observable<TextDocumentItem[] | null>
|
||||
) {
|
||||
this.subscriptions.add(
|
||||
modelTextDocuments.subscribe(docs => {
|
||||
// tslint:disable-next-line: no-floating-promises
|
||||
this.proxy.$acceptDocumentData(docs || [])
|
||||
})
|
||||
)
|
||||
|
||||
this.subscriptions.add(this.registrations)
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
|
||||
@ -1,15 +1,13 @@
|
||||
import { ProxyResult } from '@sourcegraph/comlink'
|
||||
import { isEqual } from 'lodash'
|
||||
import { from, Subscription } from 'rxjs'
|
||||
import { bufferCount, startWith } from 'rxjs/operators'
|
||||
import { createProxyAndHandleRequests } from '../../common/proxy'
|
||||
import { ExtExtensionsAPI } from '../../extension/api/extensions'
|
||||
import { Connection } from '../../protocol/jsonrpc2/connection'
|
||||
import { ExecutableExtension, ExtensionsService } from '../services/extensionsService'
|
||||
|
||||
/** @internal */
|
||||
export class ClientExtensions {
|
||||
private subscriptions = new Subscription()
|
||||
private proxy: ExtExtensionsAPI
|
||||
|
||||
/**
|
||||
* Implements the client side of the extensions API.
|
||||
@ -18,9 +16,7 @@ export class ClientExtensions {
|
||||
* @param extensions An observable that emits the set of extensions that should be activated
|
||||
* upon subscription and whenever it changes.
|
||||
*/
|
||||
constructor(connection: Connection, extensionRegistry: ExtensionsService) {
|
||||
this.proxy = createProxyAndHandleRequests('extensions', connection, this)
|
||||
|
||||
constructor(private proxy: ProxyResult<ExtExtensionsAPI>, extensionRegistry: ExtensionsService) {
|
||||
this.subscriptions.add(
|
||||
from(extensionRegistry.activeExtensions)
|
||||
.pipe(
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import { ProxyResult, ProxyValue, proxyValue, proxyValueSymbol } from '@sourcegraph/comlink'
|
||||
import { Hover, Location } from '@sourcegraph/extension-api-types'
|
||||
import { from, Observable, Subscription } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { DocumentSelector } from 'sourcegraph'
|
||||
import { createProxyAndHandleRequests } from '../../common/proxy'
|
||||
import { ExtLanguageFeaturesAPI } from '../../extension/api/languageFeatures'
|
||||
import { DocumentSelector, Unsubscribable } from 'sourcegraph'
|
||||
import { ProxySubscribable } from '../../extension/api/common'
|
||||
import { ReferenceParams, TextDocumentPositionParams, TextDocumentRegistrationOptions } from '../../protocol'
|
||||
import { Connection } from '../../protocol/jsonrpc2/connection'
|
||||
import { ProvideTextDocumentHoverSignature } from '../services/hover'
|
||||
import {
|
||||
ProvideTextDocumentLocationSignature,
|
||||
@ -13,32 +10,57 @@ import {
|
||||
TextDocumentReferencesProviderRegistry,
|
||||
} from '../services/location'
|
||||
import { FeatureProviderRegistry } from '../services/registry'
|
||||
import { SubscriptionMap } from './common'
|
||||
import { wrapRemoteObservable } from './common'
|
||||
|
||||
/** @internal */
|
||||
export interface ClientLanguageFeaturesAPI {
|
||||
$unregister(id: number): void
|
||||
$registerHoverProvider(id: number, selector: DocumentSelector): void
|
||||
$registerDefinitionProvider(id: number, selector: DocumentSelector): void
|
||||
$registerTypeDefinitionProvider(id: number, selector: DocumentSelector): void
|
||||
$registerImplementationProvider(id: number, selector: DocumentSelector): void
|
||||
$registerReferenceProvider(id: number, selector: DocumentSelector): void
|
||||
export interface ClientLanguageFeaturesAPI extends ProxyValue {
|
||||
$registerHoverProvider(
|
||||
selector: DocumentSelector,
|
||||
providerFunction: ProxyResult<
|
||||
((params: TextDocumentPositionParams) => ProxySubscribable<Hover | null | undefined>) & ProxyValue
|
||||
>
|
||||
): Unsubscribable & ProxyValue
|
||||
$registerDefinitionProvider(
|
||||
selector: DocumentSelector,
|
||||
providerFunction: ProxyResult<
|
||||
((params: TextDocumentPositionParams) => ProxySubscribable<Location[]>) & ProxyValue
|
||||
>
|
||||
): Unsubscribable & ProxyValue
|
||||
$registerTypeDefinitionProvider(
|
||||
selector: DocumentSelector,
|
||||
providerFunction: ProxyResult<
|
||||
((params: TextDocumentPositionParams) => ProxySubscribable<Location[]>) & ProxyValue
|
||||
>
|
||||
): Unsubscribable & ProxyValue
|
||||
$registerImplementationProvider(
|
||||
selector: DocumentSelector,
|
||||
providerFunction: ProxyResult<
|
||||
((params: TextDocumentPositionParams) => ProxySubscribable<Location[]>) & ProxyValue
|
||||
>
|
||||
): Unsubscribable & ProxyValue
|
||||
$registerReferenceProvider(
|
||||
selector: DocumentSelector,
|
||||
providerFunction: ProxyResult<((params: ReferenceParams) => ProxySubscribable<Location[]>) & ProxyValue>
|
||||
): Unsubscribable & ProxyValue
|
||||
|
||||
/**
|
||||
* @param idStr The `id` argument in the extension's {@link sourcegraph.languages.registerLocationProvider}
|
||||
* call.
|
||||
*/
|
||||
$registerLocationProvider(id: number, idStr: string, selector: DocumentSelector): void
|
||||
$registerLocationProvider(
|
||||
idStr: string,
|
||||
selector: DocumentSelector,
|
||||
providerFunction: ProxyResult<
|
||||
((params: TextDocumentPositionParams) => ProxySubscribable<Location[]>) & ProxyValue
|
||||
>
|
||||
): Unsubscribable & ProxyValue
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ClientLanguageFeatures implements ClientLanguageFeaturesAPI {
|
||||
private subscriptions = new Subscription()
|
||||
private registrations = new SubscriptionMap()
|
||||
private proxy: ExtLanguageFeaturesAPI
|
||||
export class ClientLanguageFeatures implements ClientLanguageFeaturesAPI, ProxyValue {
|
||||
public readonly [proxyValueSymbol] = true
|
||||
|
||||
constructor(
|
||||
connection: Connection,
|
||||
private hoverRegistry: FeatureProviderRegistry<
|
||||
TextDocumentRegistrationOptions,
|
||||
ProvideTextDocumentHoverSignature
|
||||
@ -57,93 +79,84 @@ export class ClientLanguageFeatures implements ClientLanguageFeaturesAPI {
|
||||
>,
|
||||
private referencesRegistry: TextDocumentReferencesProviderRegistry,
|
||||
private locationRegistry: TextDocumentLocationProviderIDRegistry
|
||||
) {
|
||||
this.subscriptions.add(this.registrations)
|
||||
) {}
|
||||
|
||||
this.proxy = createProxyAndHandleRequests('languageFeatures', connection, this)
|
||||
}
|
||||
|
||||
public $unregister(id: number): void {
|
||||
this.registrations.remove(id)
|
||||
}
|
||||
|
||||
public $registerHoverProvider(id: number, selector: DocumentSelector): void {
|
||||
this.registrations.add(
|
||||
id,
|
||||
this.hoverRegistry.registerProvider(
|
||||
{ documentSelector: selector },
|
||||
(params: TextDocumentPositionParams): Observable<Hover | null | undefined> =>
|
||||
from(this.proxy.$observeHover(id, params.textDocument.uri, params.position))
|
||||
public $registerHoverProvider(
|
||||
documentSelector: DocumentSelector,
|
||||
providerFunction: ProxyResult<
|
||||
((params: TextDocumentPositionParams) => ProxySubscribable<Hover | null | undefined>) & ProxyValue
|
||||
>
|
||||
): Unsubscribable & ProxyValue {
|
||||
return proxyValue(
|
||||
this.hoverRegistry.registerProvider({ documentSelector }, params =>
|
||||
wrapRemoteObservable(providerFunction(params))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
public $registerDefinitionProvider(id: number, selector: DocumentSelector): void {
|
||||
this.registrations.add(
|
||||
id,
|
||||
this.definitionRegistry.registerProvider(
|
||||
{ documentSelector: selector },
|
||||
(params: TextDocumentPositionParams): Observable<Location | Location[]> =>
|
||||
from(this.proxy.$observeDefinition(id, params.textDocument.uri, params.position)).pipe(
|
||||
map(result => result || [])
|
||||
)
|
||||
public $registerDefinitionProvider(
|
||||
documentSelector: DocumentSelector,
|
||||
providerFunction: ProxyResult<
|
||||
((params: TextDocumentPositionParams) => ProxySubscribable<Location[]>) & ProxyValue
|
||||
>
|
||||
): Unsubscribable & ProxyValue {
|
||||
return proxyValue(
|
||||
this.definitionRegistry.registerProvider({ documentSelector }, params =>
|
||||
wrapRemoteObservable(providerFunction(params))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
public $registerTypeDefinitionProvider(id: number, selector: DocumentSelector): void {
|
||||
this.registrations.add(
|
||||
id,
|
||||
this.typeDefinitionRegistry.registerProvider(
|
||||
{ documentSelector: selector },
|
||||
(params: TextDocumentPositionParams): Observable<Location | Location[]> =>
|
||||
from(this.proxy.$observeTypeDefinition(id, params.textDocument.uri, params.position)).pipe(
|
||||
map(result => result || [])
|
||||
)
|
||||
public $registerTypeDefinitionProvider(
|
||||
documentSelector: DocumentSelector,
|
||||
providerFunction: ProxyResult<
|
||||
((params: TextDocumentPositionParams) => ProxySubscribable<Location[]>) & ProxyValue
|
||||
>
|
||||
): Unsubscribable & ProxyValue {
|
||||
return proxyValue(
|
||||
this.typeDefinitionRegistry.registerProvider({ documentSelector }, params =>
|
||||
wrapRemoteObservable(providerFunction(params))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
public $registerImplementationProvider(id: number, selector: DocumentSelector): void {
|
||||
this.registrations.add(
|
||||
id,
|
||||
this.implementationRegistry.registerProvider(
|
||||
{ documentSelector: selector },
|
||||
(params: TextDocumentPositionParams): Observable<Location | Location[]> =>
|
||||
from(this.proxy.$observeImplementation(id, params.textDocument.uri, params.position)).pipe(
|
||||
map(result => result || [])
|
||||
)
|
||||
public $registerImplementationProvider(
|
||||
documentSelector: DocumentSelector,
|
||||
providerFunction: ProxyResult<
|
||||
((params: TextDocumentPositionParams) => ProxySubscribable<Location[]>) & ProxyValue
|
||||
>
|
||||
): Unsubscribable & ProxyValue {
|
||||
return proxyValue(
|
||||
this.implementationRegistry.registerProvider({ documentSelector }, params =>
|
||||
wrapRemoteObservable(providerFunction(params))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
public $registerReferenceProvider(id: number, selector: DocumentSelector): void {
|
||||
this.registrations.add(
|
||||
id,
|
||||
this.referencesRegistry.registerProvider(
|
||||
{ documentSelector: selector },
|
||||
(params: ReferenceParams): Observable<Location[]> =>
|
||||
from(
|
||||
this.proxy.$observeReferences(id, params.textDocument.uri, params.position, params.context)
|
||||
).pipe(map(result => result || []))
|
||||
public $registerReferenceProvider(
|
||||
documentSelector: DocumentSelector,
|
||||
providerFunction: ProxyResult<
|
||||
((params: TextDocumentPositionParams) => ProxySubscribable<Location[]>) & ProxyValue
|
||||
>
|
||||
): Unsubscribable & ProxyValue {
|
||||
return proxyValue(
|
||||
this.referencesRegistry.registerProvider({ documentSelector }, params =>
|
||||
wrapRemoteObservable(providerFunction(params))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
public $registerLocationProvider(id: number, idStr: string, selector: DocumentSelector): void {
|
||||
this.registrations.add(
|
||||
id,
|
||||
this.locationRegistry.registerProvider(
|
||||
{ id: idStr, documentSelector: selector },
|
||||
(params: TextDocumentPositionParams): Observable<Location[]> =>
|
||||
from(this.proxy.$observeLocations(id, params.textDocument.uri, params.position)).pipe(
|
||||
map(result => result || [])
|
||||
)
|
||||
public $registerLocationProvider(
|
||||
id: string,
|
||||
documentSelector: DocumentSelector,
|
||||
providerFunction: ProxyResult<
|
||||
((params: TextDocumentPositionParams) => ProxySubscribable<Location[]>) & ProxyValue
|
||||
>
|
||||
): Unsubscribable & ProxyValue {
|
||||
return proxyValue(
|
||||
this.locationRegistry.registerProvider({ id, documentSelector }, params =>
|
||||
wrapRemoteObservable(providerFunction(params))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
this.subscriptions.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,17 @@
|
||||
import { ProxyResult } from '@sourcegraph/comlink'
|
||||
import { WorkspaceRoot } from '@sourcegraph/extension-api-types'
|
||||
import { Observable, Subscription } from 'rxjs'
|
||||
import { createProxyAndHandleRequests } from '../../common/proxy'
|
||||
import { ExtRootsAPI } from '../../extension/api/roots'
|
||||
import { Connection } from '../../protocol/jsonrpc2/connection'
|
||||
|
||||
/** @internal */
|
||||
export class ClientRoots {
|
||||
private subscriptions = new Subscription()
|
||||
private proxy: ExtRootsAPI
|
||||
|
||||
constructor(connection: Connection, modelRoots: Observable<WorkspaceRoot[] | null>) {
|
||||
this.proxy = createProxyAndHandleRequests('roots', connection, this)
|
||||
|
||||
constructor(proxy: ProxyResult<ExtRootsAPI>, modelRoots: Observable<WorkspaceRoot[] | null>) {
|
||||
this.subscriptions.add(
|
||||
modelRoots.subscribe(roots => {
|
||||
this.proxy.$acceptRoots(roots || [])
|
||||
// tslint:disable-next-line: no-floating-promises
|
||||
proxy.$acceptRoots(roots || [])
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,47 +1,25 @@
|
||||
import { from, Observable, Subscription } from 'rxjs'
|
||||
import { createProxyAndHandleRequests } from '../../common/proxy'
|
||||
import { ExtSearch } from '../../extension/api/search'
|
||||
import { Connection } from '../../protocol/jsonrpc2/connection'
|
||||
import { ProxyResult, ProxyValue, proxyValue, proxyValueSymbol } from '@sourcegraph/comlink'
|
||||
import { from } from 'rxjs'
|
||||
import { QueryTransformer, Unsubscribable } from 'sourcegraph'
|
||||
import { TransformQuerySignature } from '../services/queryTransformer'
|
||||
import { FeatureProviderRegistry } from '../services/registry'
|
||||
import { SubscriptionMap } from './common'
|
||||
|
||||
/** @internal */
|
||||
export interface SearchAPI {
|
||||
$registerQueryTransformer(id: number): void
|
||||
$unregister(id: number): void
|
||||
export interface ClientSearchAPI extends ProxyValue {
|
||||
$registerQueryTransformer(transformer: ProxyResult<QueryTransformer & ProxyValue>): Unsubscribable & ProxyValue
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class Search implements SearchAPI {
|
||||
private subscriptions = new Subscription()
|
||||
private registrations = new SubscriptionMap()
|
||||
private proxy: ExtSearch
|
||||
export class ClientSearch implements ClientSearchAPI, ProxyValue {
|
||||
public readonly [proxyValueSymbol] = true
|
||||
|
||||
constructor(
|
||||
connection: Connection,
|
||||
private queryTransformerRegistry: FeatureProviderRegistry<{}, TransformQuerySignature>
|
||||
) {
|
||||
this.subscriptions.add(this.registrations)
|
||||
constructor(private queryTransformerRegistry: FeatureProviderRegistry<{}, TransformQuerySignature>) {}
|
||||
|
||||
this.proxy = createProxyAndHandleRequests('search', connection, this)
|
||||
}
|
||||
|
||||
public $registerQueryTransformer(id: number): void {
|
||||
this.registrations.add(
|
||||
id,
|
||||
this.queryTransformerRegistry.registerProvider(
|
||||
{},
|
||||
(query: string): Observable<string> => from(this.proxy.$transformQuery(id, query))
|
||||
)
|
||||
public $registerQueryTransformer(
|
||||
transformer: ProxyResult<QueryTransformer & ProxyValue>
|
||||
): Unsubscribable & ProxyValue {
|
||||
return proxyValue(
|
||||
this.queryTransformerRegistry.registerProvider({}, query => from(transformer.transformQuery(query)))
|
||||
)
|
||||
}
|
||||
|
||||
public $unregister(id: number): void {
|
||||
this.registrations.remove(id)
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
this.subscriptions.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,48 +1,36 @@
|
||||
import { ReplaySubject, Subject, Subscription } from 'rxjs'
|
||||
import { ProxyValue, proxyValue, proxyValueSymbol } from '@sourcegraph/comlink'
|
||||
import { ReplaySubject, Unsubscribable } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { PanelView } from 'sourcegraph'
|
||||
import { handleRequests } from '../../common/proxy'
|
||||
import { ContributableViewContainer, TextDocumentPositionParams } from '../../protocol'
|
||||
import { Connection } from '../../protocol/jsonrpc2/connection'
|
||||
import { TextDocumentLocationProviderIDRegistry } from '../services/location'
|
||||
import { PanelViewWithComponent, ViewProviderRegistry } from '../services/view'
|
||||
import { SubscriptionMap } from './common'
|
||||
|
||||
/** @internal */
|
||||
export interface PanelViewData extends Pick<PanelView, 'title' | 'content' | 'priority' | 'component'> {}
|
||||
|
||||
export interface PanelUpdater extends Unsubscribable, ProxyValue {
|
||||
update(data: PanelViewData): void
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface ClientViewsAPI {
|
||||
$unregister(id: number): void
|
||||
$registerPanelViewProvider(id: number, provider: { id: string }): void
|
||||
$acceptPanelViewUpdate(id: number, params: Partial<PanelViewData>): void
|
||||
export interface ClientViewsAPI extends ProxyValue {
|
||||
$registerPanelViewProvider(provider: { id: string }): PanelUpdater
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ClientViews implements ClientViewsAPI {
|
||||
private subscriptions = new Subscription()
|
||||
private panelViews = new Map<number, Subject<PanelViewData>>()
|
||||
private registrations = new SubscriptionMap()
|
||||
public readonly [proxyValueSymbol] = true
|
||||
|
||||
constructor(
|
||||
connection: Connection,
|
||||
private viewRegistry: ViewProviderRegistry,
|
||||
private textDocumentLocations: TextDocumentLocationProviderIDRegistry
|
||||
) {
|
||||
this.subscriptions.add(this.registrations)
|
||||
) {}
|
||||
|
||||
handleRequests(connection, 'views', this)
|
||||
}
|
||||
|
||||
public $unregister(id: number): void {
|
||||
this.registrations.remove(id)
|
||||
}
|
||||
|
||||
public $registerPanelViewProvider(id: number, provider: { id: string }): void {
|
||||
public $registerPanelViewProvider(provider: { id: string }): PanelUpdater {
|
||||
// TODO(sqs): This will probably hang forever if an extension neglects to set any of the fields on a
|
||||
// PanelView because this subject will never emit.
|
||||
const panelView = new ReplaySubject<PanelViewData>(1)
|
||||
this.panelViews.set(id, panelView)
|
||||
const registryUnsubscribable = this.viewRegistry.registerProvider(
|
||||
{ ...provider, container: ContributableViewContainer.Panel },
|
||||
panelView.pipe(
|
||||
@ -60,23 +48,13 @@ export class ClientViews implements ClientViewsAPI {
|
||||
)
|
||||
)
|
||||
)
|
||||
this.registrations.add(id, {
|
||||
return proxyValue({
|
||||
update: (data: PanelViewData) => {
|
||||
panelView.next(data)
|
||||
},
|
||||
unsubscribe: () => {
|
||||
registryUnsubscribable.unsubscribe()
|
||||
this.panelViews.delete(id)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
public $acceptPanelViewUpdate(id: number, data: PanelViewData): void {
|
||||
const panelView = this.panelViews.get(id)
|
||||
if (panelView === undefined) {
|
||||
throw new Error(`no panel view with ID ${id}`)
|
||||
}
|
||||
panelView.next(data)
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
this.subscriptions.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { ProxyResult, ProxyValue, proxyValue, proxyValueSymbol } from '@sourcegraph/comlink'
|
||||
import { Observable, Subject, Subscription } from 'rxjs'
|
||||
import * as sourcegraph from 'sourcegraph'
|
||||
import { createProxyAndHandleRequests } from '../../common/proxy'
|
||||
import { ExtWindowsAPI } from '../../extension/api/windows'
|
||||
import { Connection } from '../../protocol/jsonrpc2/connection'
|
||||
import { ViewComponentData } from '../model'
|
||||
import {
|
||||
MessageActionItem,
|
||||
@ -11,25 +10,23 @@ import {
|
||||
ShowMessageParams,
|
||||
ShowMessageRequestParams,
|
||||
} from '../services/notifications'
|
||||
import { SubscriptionMap } from './common'
|
||||
|
||||
/** @internal */
|
||||
export interface ClientWindowsAPI {
|
||||
export interface ClientWindowsAPI extends ProxyValue {
|
||||
$showNotification(message: string): void
|
||||
$showMessage(message: string): Promise<void>
|
||||
$showInputBox(options?: sourcegraph.InputBoxOptions): Promise<string | undefined>
|
||||
$startProgress(options: sourcegraph.ProgressOptions): Promise<number>
|
||||
$updateProgress(handle: number, progress?: sourcegraph.Progress, error?: any, done?: boolean): void
|
||||
$showProgress(options: sourcegraph.ProgressOptions): sourcegraph.ProgressReporter & ProxyValue
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ClientWindows implements ClientWindowsAPI {
|
||||
public readonly [proxyValueSymbol] = true
|
||||
|
||||
private subscriptions = new Subscription()
|
||||
private registrations = new SubscriptionMap()
|
||||
private proxy: ExtWindowsAPI
|
||||
|
||||
constructor(
|
||||
connection: Connection,
|
||||
private proxy: ProxyResult<ExtWindowsAPI>,
|
||||
modelVisibleViewComponents: Observable<ViewComponentData[] | null>,
|
||||
/** Called when the client receives a window/showMessage notification. */
|
||||
private showMessage: (params: ShowMessageParams) => void,
|
||||
@ -45,10 +42,9 @@ export class ClientWindows implements ClientWindowsAPI {
|
||||
private showInput: (params: ShowInputParams) => Promise<string | null>,
|
||||
private createProgressReporter: (options: sourcegraph.ProgressOptions) => Subject<sourcegraph.Progress>
|
||||
) {
|
||||
this.proxy = createProxyAndHandleRequests('windows', connection, this)
|
||||
|
||||
this.subscriptions.add(
|
||||
modelVisibleViewComponents.subscribe(viewComponents => {
|
||||
// tslint:disable-next-line: no-floating-promises
|
||||
this.proxy.$acceptWindowData(
|
||||
viewComponents
|
||||
? [
|
||||
@ -64,12 +60,10 @@ export class ClientWindows implements ClientWindowsAPI {
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
this.subscriptions.add(this.registrations)
|
||||
}
|
||||
|
||||
public $showNotification(message: string): void {
|
||||
return this.showMessage({ type: MessageType.Info, message })
|
||||
this.showMessage({ type: MessageType.Info, message })
|
||||
}
|
||||
|
||||
public $showMessage(message: string): Promise<void> {
|
||||
@ -92,29 +86,8 @@ export class ClientWindows implements ClientWindowsAPI {
|
||||
)
|
||||
}
|
||||
|
||||
private handles = 1
|
||||
private progressReporters = new Map<number, Subject<sourcegraph.Progress>>()
|
||||
|
||||
public async $startProgress(options: sourcegraph.ProgressOptions): Promise<number> {
|
||||
const handle = this.handles++
|
||||
const reporter = this.createProgressReporter(options)
|
||||
this.progressReporters.set(handle, reporter)
|
||||
return handle
|
||||
}
|
||||
|
||||
public $updateProgress(handle: number, progress?: sourcegraph.Progress, error?: any, done?: boolean): void {
|
||||
const reporter = this.progressReporters.get(handle)
|
||||
if (!reporter) {
|
||||
console.warn('No ProgressReporter for handle ' + handle)
|
||||
return
|
||||
}
|
||||
if (done || (progress && progress.percentage && progress.percentage >= 100)) {
|
||||
reporter.complete()
|
||||
} else if (error) {
|
||||
reporter.error(error)
|
||||
} else if (progress) {
|
||||
reporter.next(progress)
|
||||
}
|
||||
public $showProgress(options: sourcegraph.ProgressOptions): sourcegraph.ProgressReporter & ProxyValue {
|
||||
return proxyValue(this.createProgressReporter(options))
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { Observable, Subscription, Unsubscribable } from 'rxjs'
|
||||
import { from, merge, Observable, Unsubscribable } from 'rxjs'
|
||||
import { switchMap } from 'rxjs/operators'
|
||||
import { Connection } from '../protocol/jsonrpc2/connection'
|
||||
import { createExtensionHostClientConnection, ExtensionHostClientConnection } from './connection'
|
||||
import { EndpointPair } from '../../platform/context'
|
||||
import { InitData } from '../extension/extensionHost'
|
||||
import { createExtensionHostClientConnection } from './connection'
|
||||
import { Services } from './services'
|
||||
|
||||
export interface ExtensionHostClient extends Unsubscribable {
|
||||
@ -15,27 +16,20 @@ export interface ExtensionHostClient extends Unsubscribable {
|
||||
/**
|
||||
* Creates a client to communicate with an extension host.
|
||||
*
|
||||
* @param extensionHostConnection An observable that emits the connection to the extension host each time a new
|
||||
* @param extensionHostEndpoint An observable that emits the connection to the extension host each time a new
|
||||
* connection is established.
|
||||
*/
|
||||
export function createExtensionHostClient(
|
||||
services: Services,
|
||||
extensionHostConnection: Observable<Connection>
|
||||
extensionHostEndpoint: Observable<EndpointPair>,
|
||||
initData: InitData
|
||||
): ExtensionHostClient {
|
||||
const subscriptions = new Subscription()
|
||||
|
||||
const client = extensionHostConnection.pipe(
|
||||
switchMap(connection => {
|
||||
const client = createExtensionHostClientConnection(connection, services)
|
||||
return new Observable<ExtensionHostClientConnection>(sub => {
|
||||
sub.next(client)
|
||||
return () => client.unsubscribe()
|
||||
})
|
||||
})
|
||||
const client = extensionHostEndpoint.pipe(
|
||||
switchMap(endpoints =>
|
||||
from(createExtensionHostClientConnection(endpoints, services, initData)).pipe(
|
||||
switchMap(client => merge([client], new Observable<never>(() => client)))
|
||||
)
|
||||
)
|
||||
)
|
||||
subscriptions.add(client.subscribe())
|
||||
|
||||
return {
|
||||
unsubscribe: () => subscriptions.unsubscribe(),
|
||||
}
|
||||
return client.subscribe()
|
||||
}
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import * as comlink from '@sourcegraph/comlink'
|
||||
import { isEqual } from 'lodash'
|
||||
import { from, Subject, Subscription } from 'rxjs'
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators'
|
||||
import { ContextValues, Progress, ProgressOptions } from 'sourcegraph'
|
||||
import { Connection } from '../protocol/jsonrpc2/connection'
|
||||
import { Tracer } from '../protocol/jsonrpc2/trace'
|
||||
import { ContextValues, Progress, ProgressOptions, Unsubscribable } from 'sourcegraph'
|
||||
import { EndpointPair } from '../../platform/context'
|
||||
import { ExtensionHostAPIFactory } from '../extension/api/api'
|
||||
import { InitData } from '../extension/extensionHost'
|
||||
import { ClientAPI } from './api/api'
|
||||
import { ClientCodeEditor } from './api/codeEditor'
|
||||
import { ClientCommands } from './api/commands'
|
||||
import { ClientConfiguration } from './api/configuration'
|
||||
@ -11,7 +15,7 @@ import { ClientDocuments } from './api/documents'
|
||||
import { ClientExtensions } from './api/extensions'
|
||||
import { ClientLanguageFeatures } from './api/languageFeatures'
|
||||
import { ClientRoots } from './api/roots'
|
||||
import { Search } from './api/search'
|
||||
import { ClientSearch } from './api/search'
|
||||
import { ClientViews } from './api/views'
|
||||
import { ClientWindows } from './api/windows'
|
||||
import { applyContextUpdate } from './context/context'
|
||||
@ -24,12 +28,6 @@ import {
|
||||
} from './services/notifications'
|
||||
|
||||
export interface ExtensionHostClientConnection {
|
||||
/**
|
||||
* Sets or unsets the tracer to use for logging all of this client's messages to/from the
|
||||
* extension host.
|
||||
*/
|
||||
setTracer(tracer: Tracer | null): void
|
||||
|
||||
/**
|
||||
* Closes the connection to and terminates the extension host.
|
||||
*/
|
||||
@ -51,85 +49,101 @@ export interface ActivatedExtension {
|
||||
deactivate(): void | Promise<void>
|
||||
}
|
||||
|
||||
export function createExtensionHostClientConnection(
|
||||
connection: Connection,
|
||||
services: Services
|
||||
): ExtensionHostClientConnection {
|
||||
/**
|
||||
* @param endpoint The Worker object to communicate with
|
||||
*/
|
||||
export async function createExtensionHostClientConnection(
|
||||
endpoints: EndpointPair,
|
||||
services: Services,
|
||||
initData: InitData
|
||||
): Promise<Unsubscribable> {
|
||||
const subscription = new Subscription()
|
||||
|
||||
connection.onRequest('ping', () => 'pong')
|
||||
// MAIN THREAD
|
||||
|
||||
subscription.add(new ClientConfiguration<any>(connection, services.settings))
|
||||
subscription.add(
|
||||
new ClientContext(connection, (updates: ContextValues) =>
|
||||
services.context.data.next(applyContextUpdate(services.context.data.value, updates))
|
||||
)
|
||||
/** Proxy to the exposed extension host API */
|
||||
const initializeExtensionHost = comlink.proxy<ExtensionHostAPIFactory>(endpoints.proxy)
|
||||
const proxy = await initializeExtensionHost(initData)
|
||||
|
||||
const clientConfiguration = new ClientConfiguration<any>(proxy.configuration, services.settings)
|
||||
subscription.add(clientConfiguration)
|
||||
|
||||
const clientContext = new ClientContext((updates: ContextValues) =>
|
||||
services.context.data.next(applyContextUpdate(services.context.data.value, updates))
|
||||
)
|
||||
subscription.add(
|
||||
new ClientWindows(
|
||||
connection,
|
||||
from(services.model.model).pipe(
|
||||
map(({ visibleViewComponents }) => visibleViewComponents),
|
||||
distinctUntilChanged()
|
||||
),
|
||||
(params: ShowMessageParams) => services.notifications.showMessages.next({ ...params }),
|
||||
(params: ShowMessageRequestParams) =>
|
||||
new Promise<MessageActionItem | null>(resolve => {
|
||||
services.notifications.showMessageRequests.next({ ...params, resolve })
|
||||
}),
|
||||
(params: ShowInputParams) =>
|
||||
new Promise<string | null>(resolve => {
|
||||
services.notifications.showInputs.next({ ...params, resolve })
|
||||
}),
|
||||
({ title }: ProgressOptions) => {
|
||||
const reporter = new Subject<Progress>()
|
||||
services.notifications.progresses.next({ title, progress: reporter.asObservable() })
|
||||
return reporter
|
||||
}
|
||||
)
|
||||
subscription.add(clientContext)
|
||||
const clientWindows = new ClientWindows(
|
||||
proxy.windows,
|
||||
from(services.model.model).pipe(
|
||||
map(({ visibleViewComponents }) => visibleViewComponents),
|
||||
distinctUntilChanged()
|
||||
),
|
||||
(params: ShowMessageParams) => services.notifications.showMessages.next({ ...params }),
|
||||
(params: ShowMessageRequestParams) =>
|
||||
new Promise<MessageActionItem | null>(resolve => {
|
||||
services.notifications.showMessageRequests.next({ ...params, resolve })
|
||||
}),
|
||||
(params: ShowInputParams) =>
|
||||
new Promise<string | null>(resolve => {
|
||||
services.notifications.showInputs.next({ ...params, resolve })
|
||||
}),
|
||||
({ title }: ProgressOptions) => {
|
||||
const reporter = new Subject<Progress>()
|
||||
services.notifications.progresses.next({ title, progress: reporter.asObservable() })
|
||||
return reporter
|
||||
}
|
||||
)
|
||||
subscription.add(new ClientViews(connection, services.views, services.textDocumentLocations))
|
||||
subscription.add(new ClientCodeEditor(connection, services.textDocumentDecoration))
|
||||
subscription.add(clientWindows)
|
||||
|
||||
const clientViews = new ClientViews(services.views, services.textDocumentLocations)
|
||||
|
||||
const clientCodeEditor = new ClientCodeEditor(services.textDocumentDecoration)
|
||||
subscription.add(clientCodeEditor)
|
||||
subscription.add(
|
||||
new ClientDocuments(
|
||||
connection,
|
||||
proxy.documents,
|
||||
from(services.model.model).pipe(
|
||||
map(
|
||||
({ visibleViewComponents }) =>
|
||||
visibleViewComponents && visibleViewComponents.map(({ item }) => item)
|
||||
),
|
||||
distinctUntilChanged()
|
||||
distinctUntilChanged((a, b) => isEqual(a, b))
|
||||
)
|
||||
)
|
||||
)
|
||||
subscription.add(
|
||||
new ClientLanguageFeatures(
|
||||
connection,
|
||||
services.textDocumentHover,
|
||||
services.textDocumentDefinition,
|
||||
services.textDocumentTypeDefinition,
|
||||
services.textDocumentImplementation,
|
||||
services.textDocumentReferences,
|
||||
services.textDocumentLocations
|
||||
)
|
||||
const clientLanguageFeatures = new ClientLanguageFeatures(
|
||||
services.textDocumentHover,
|
||||
services.textDocumentDefinition,
|
||||
services.textDocumentTypeDefinition,
|
||||
services.textDocumentImplementation,
|
||||
services.textDocumentReferences,
|
||||
services.textDocumentLocations
|
||||
)
|
||||
subscription.add(new Search(connection, services.queryTransformer))
|
||||
subscription.add(new ClientCommands(connection, services.commands))
|
||||
const clientSearch = new ClientSearch(services.queryTransformer)
|
||||
const clientCommands = new ClientCommands(services.commands)
|
||||
subscription.add(
|
||||
new ClientRoots(
|
||||
connection,
|
||||
proxy.roots,
|
||||
from(services.model.model).pipe(
|
||||
map(({ roots }) => roots),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
)
|
||||
)
|
||||
subscription.add(new ClientExtensions(connection, services.extensions))
|
||||
subscription.add(new ClientExtensions(proxy.extensions, services.extensions))
|
||||
|
||||
return {
|
||||
setTracer: tracer => {
|
||||
connection.trace(tracer)
|
||||
},
|
||||
unsubscribe: () => subscription.unsubscribe(),
|
||||
const clientAPI: ClientAPI = {
|
||||
ping: () => 'pong',
|
||||
context: clientContext,
|
||||
search: clientSearch,
|
||||
configuration: clientConfiguration,
|
||||
languageFeatures: clientLanguageFeatures,
|
||||
commands: clientCommands,
|
||||
windows: clientWindows,
|
||||
codeEditor: clientCodeEditor,
|
||||
views: clientViews,
|
||||
}
|
||||
comlink.expose(clientAPI, endpoints.expose)
|
||||
|
||||
return subscription
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import { EMPTY_MODEL, Model } from '../model'
|
||||
* The model service manages the model of documents and roots.
|
||||
*/
|
||||
export interface ModelService {
|
||||
model: Subscribable<Model> & { value: Model } & NextObserver<Model>
|
||||
readonly model: Subscribable<Model> & { readonly value: Model } & NextObserver<Model>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Observable, of } from 'rxjs'
|
||||
import { flatMap, map, switchMap } from 'rxjs/operators'
|
||||
import { mergeMap, switchMap } from 'rxjs/operators'
|
||||
import { FeatureProviderRegistry } from './registry'
|
||||
|
||||
export type TransformQuerySignature = (query: string) => Observable<string>
|
||||
@ -24,9 +24,8 @@ export function transformQuery(providers: Observable<TransformQuerySignature[]>,
|
||||
if (providers.length === 0) {
|
||||
return [query]
|
||||
}
|
||||
return providers.reduce(
|
||||
(currentQuery, transformQuery) =>
|
||||
currentQuery.pipe(flatMap(q => transformQuery(q).pipe(map(transformedQuery => transformedQuery)))),
|
||||
return providers.reduce<Observable<string>>(
|
||||
(currentQuery, transformQuery) => currentQuery.pipe(mergeMap(q => transformQuery(q))),
|
||||
of(query)
|
||||
)
|
||||
})
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
import { Observable } from 'rxjs'
|
||||
import { bufferCount, first } from 'rxjs/operators'
|
||||
import { createConnection } from '../protocol/jsonrpc2/connection'
|
||||
import { createMessageTransports } from '../protocol/jsonrpc2/testHelpers'
|
||||
import { createProxy, handleRequests } from './proxy'
|
||||
|
||||
function createTestProxy<T>(handler: T): Record<keyof T, (...args: any[]) => any> {
|
||||
Object.setPrototypeOf(handler, null)
|
||||
const [clientTransports, serverTransports] = createMessageTransports()
|
||||
const client = createConnection(clientTransports)
|
||||
const server = createConnection(serverTransports)
|
||||
|
||||
const proxy = createProxy(client, 'prefix')
|
||||
handleRequests(server, 'prefix', handler)
|
||||
client.listen()
|
||||
server.listen()
|
||||
return proxy
|
||||
}
|
||||
|
||||
describe('Proxy', () => {
|
||||
describe('proxies calls', () => {
|
||||
const proxy = createTestProxy({
|
||||
$a: (n: number) => n + 1,
|
||||
$b: (n: number) => Promise.resolve(n + 2),
|
||||
$c: (m: number, n: number) => m + n,
|
||||
$d: (...args: number[]) => args.reduce((sum, n) => sum + n, 0),
|
||||
$e: () => Promise.reject('e'),
|
||||
$f: () => {
|
||||
throw new Error('f')
|
||||
},
|
||||
})
|
||||
|
||||
test('to functions', async () => expect(proxy.$a(1)).resolves.toBe(2))
|
||||
test('to async functions', async () => expect(proxy.$b(1)).resolves.toBe(3))
|
||||
test('with multiple arguments ', async () => expect(proxy.$c(2, 3)).resolves.toBe(5))
|
||||
test('with variadic arguments ', async () => expect(proxy.$d(...[2, 3, 4])).resolves.toBe(9))
|
||||
test('to functions returning a rejected promise', async () =>
|
||||
expect(proxy.$e()).rejects.toMatchObject({
|
||||
message: 'Request prefix/$e failed unexpectedly without providing any details.',
|
||||
}))
|
||||
test('to functions throwing an error', async () => expect(proxy.$f()).rejects.toMatchObject({ message: 'f' }))
|
||||
})
|
||||
|
||||
test('proxies Observables', async () => {
|
||||
const proxy = createTestProxy({
|
||||
$observe: (...args: number[]) =>
|
||||
new Observable<number>(observer => {
|
||||
for (const arg of args) {
|
||||
observer.next(arg + 1)
|
||||
}
|
||||
observer.complete()
|
||||
}),
|
||||
})
|
||||
|
||||
await expect(
|
||||
proxy
|
||||
.$observe(1, 2, 3, 4)
|
||||
.pipe(
|
||||
bufferCount(4),
|
||||
first()
|
||||
)
|
||||
.toPromise()
|
||||
).resolves.toEqual([2, 3, 4, 5])
|
||||
})
|
||||
})
|
||||
@ -1,51 +0,0 @@
|
||||
import { Connection } from '../protocol/jsonrpc2/connection'
|
||||
|
||||
/**
|
||||
* @returns A proxy that translates method calls on itself to requests sent on the {@link connection}.
|
||||
*/
|
||||
export function createProxyAndHandleRequests(prefix: string, connection: Connection, handler: any): any {
|
||||
handleRequests(connection, prefix, handler)
|
||||
return createProxy(connection, prefix)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Proxy that translates method calls (whose name begins with "$") on the returned object to messages
|
||||
* named `${prefix}/${name}` on the connection.
|
||||
*
|
||||
* @param connection - The connection to send messages on when proxy methods are called.
|
||||
* @param prefix - The name prefix for connection methods.
|
||||
*/
|
||||
export function createProxy(connection: Connection, prefix: string): any {
|
||||
return new Proxy(Object.create(null), {
|
||||
get: (target: any, name: string) => {
|
||||
if (!target[name] && name[0] === '$') {
|
||||
const method = `${prefix}/${name}`
|
||||
if (name.startsWith('$observe')) {
|
||||
target[name] = (...args: any[]) => connection.observeRequest(method, args)
|
||||
} else {
|
||||
target[name] = (...args: any[]) => connection.sendRequest(method, args)
|
||||
}
|
||||
}
|
||||
return target[name]
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwards all requests received on the connection to the corresponding method on the handler object. The
|
||||
* connection method `${prefix}/${name}` corresponds to the `${name}` method on the handler object.
|
||||
*
|
||||
* @param handler - An instance of a class whose methods should be called when the connection receives
|
||||
* corresponding requests, or an object created with Object.create(null) (or otherwise with a null
|
||||
* prototype) whose properties contain functions to be called.
|
||||
*/
|
||||
export function handleRequests(connection: Connection, prefix: string, handler: any): void {
|
||||
// A class instance's methods are own, non-enumerable properties of its prototype.
|
||||
const proto = Object.getPrototypeOf(handler) || handler
|
||||
for (const name of Object.getOwnPropertyNames(proto)) {
|
||||
const value = proto[name]
|
||||
if (name[0] === '$' && typeof value === 'function') {
|
||||
connection.onRequest(`${prefix}/${name}`, (args: any[]) => value.apply(handler, args))
|
||||
}
|
||||
}
|
||||
}
|
||||
19
shared/src/api/extension/api/api.ts
Normal file
19
shared/src/api/extension/api/api.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { ProxyValue } from '@sourcegraph/comlink'
|
||||
import { InitData } from '../extensionHost'
|
||||
import { ExtConfigurationAPI } from './configuration'
|
||||
import { ExtDocumentsAPI } from './documents'
|
||||
import { ExtExtensionsAPI } from './extensions'
|
||||
import { ExtRootsAPI } from './roots'
|
||||
import { ExtWindowsAPI } from './windows'
|
||||
|
||||
export type ExtensionHostAPIFactory = (initData: InitData) => ExtensionHostAPI
|
||||
|
||||
export interface ExtensionHostAPI extends ProxyValue {
|
||||
ping(): 'pong'
|
||||
|
||||
documents: ExtDocumentsAPI
|
||||
extensions: ExtExtensionsAPI
|
||||
roots: ExtRootsAPI
|
||||
windows: ExtWindowsAPI
|
||||
configuration: ExtConfigurationAPI<any>
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import { ProxyResult } from '@sourcegraph/comlink'
|
||||
import * as clientType from '@sourcegraph/extension-api-types'
|
||||
import { of } from 'rxjs'
|
||||
import * as sourcegraph from 'sourcegraph'
|
||||
@ -15,7 +16,7 @@ export class ExtCodeEditor implements sourcegraph.CodeEditor {
|
||||
private resource: string,
|
||||
public _selections: clientType.Selection[],
|
||||
public readonly isActive: boolean,
|
||||
private proxy: ClientCodeEditorAPI,
|
||||
private proxy: ProxyResult<ClientCodeEditorAPI>,
|
||||
private documents: ExtDocuments
|
||||
) {}
|
||||
|
||||
@ -42,6 +43,7 @@ export class ExtCodeEditor implements sourcegraph.CodeEditor {
|
||||
// Backcompat: extensions developed against an older version of the API
|
||||
// may not supply a decorationType
|
||||
decorationType = decorationType || DEFAULT_DECORATION_TYPE
|
||||
// tslint:disable-next-line: no-floating-promises
|
||||
this.proxy.$setDecorations(this.resource, decorationType.key, decorations.map(fromTextDocumentDecoration))
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,7 @@
|
||||
import { ProxyResult, proxyValue } from '@sourcegraph/comlink'
|
||||
import { Unsubscribable } from 'rxjs'
|
||||
import { ClientCommandsAPI } from '../../client/api/commands'
|
||||
import { ProviderMap } from './common'
|
||||
|
||||
/** @internal */
|
||||
export interface ExtCommandsAPI {
|
||||
$executeCommand(id: number, args: any[]): Promise<any>
|
||||
}
|
||||
import { syncSubscription } from '../../util'
|
||||
|
||||
interface CommandEntry {
|
||||
command: string
|
||||
@ -13,16 +9,8 @@ interface CommandEntry {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ExtCommands implements ExtCommandsAPI, Unsubscribable {
|
||||
private registrations = new ProviderMap<CommandEntry>(id => this.proxy.$unregister(id))
|
||||
|
||||
constructor(private proxy: ClientCommandsAPI) {}
|
||||
|
||||
/** Proxy method invoked by the client when the client wants to executes a command. */
|
||||
public $executeCommand(id: number, args: any[]): Promise<any> {
|
||||
const { callback } = this.registrations.get<CommandEntry>(id)
|
||||
return Promise.resolve(callback(...args))
|
||||
}
|
||||
export class ExtCommands {
|
||||
constructor(private proxy: ProxyResult<ClientCommandsAPI>) {}
|
||||
|
||||
/**
|
||||
* Extension API method invoked directly when an extension wants to execute a command. It calls to the client
|
||||
@ -34,12 +22,6 @@ export class ExtCommands implements ExtCommandsAPI, Unsubscribable {
|
||||
}
|
||||
|
||||
public registerCommand(entry: CommandEntry): Unsubscribable {
|
||||
const { id, subscription } = this.registrations.add(entry)
|
||||
this.proxy.$registerCommand(id, entry.command)
|
||||
return subscription
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
this.registrations.unsubscribe()
|
||||
return syncSubscription(this.proxy.$registerCommand(entry.command, proxyValue(entry.callback)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,99 +1,61 @@
|
||||
import { from, isObservable, Observable, of, Subscribable as RxJSSubscribable } from 'rxjs'
|
||||
import { ProxyResult, ProxyValue, proxyValue, proxyValueSymbol, UnproxyOrClone } from '@sourcegraph/comlink'
|
||||
import { from, isObservable, Observable, Observer, of } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { Subscribable, Unsubscribable } from 'sourcegraph'
|
||||
import { ProviderResult, Subscribable, Unsubscribable } from 'sourcegraph'
|
||||
import { isPromise, isSubscribable } from '../../util'
|
||||
|
||||
/**
|
||||
* Manages a set of providers and associates a unique ID with each.
|
||||
*
|
||||
* @template B - The base provider type.
|
||||
* @internal
|
||||
* A Subscribable that can be exposed by comlink to the other thread.
|
||||
* Only allows full object Observers to avoid complex type checking against proxies.
|
||||
*/
|
||||
export class ProviderMap<B> {
|
||||
private idSequence = 0
|
||||
private map = new Map<number, B>()
|
||||
|
||||
/**
|
||||
* @param unsubscribeProvider - Callback to unsubscribe a provider.
|
||||
*/
|
||||
constructor(private unsubscribeProvider: (id: number) => void) {}
|
||||
|
||||
/**
|
||||
* Adds a new provider.
|
||||
*
|
||||
* @param provider - The provider to add.
|
||||
* @returns A newly allocated ID for the provider, unique among all other IDs in this map, and an
|
||||
* unsubscribable for the provider.
|
||||
* @throws If there already exists an entry with the given {@link id}.
|
||||
*/
|
||||
public add(provider: B): { id: number; subscription: Unsubscribable } {
|
||||
const id = this.idSequence
|
||||
this.map.set(id, provider)
|
||||
this.idSequence++
|
||||
return { id, subscription: { unsubscribe: () => this.remove(id) } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provider with the given {@link id}.
|
||||
*
|
||||
* @template P - The specific provider type for the provider with this {@link id}.
|
||||
* @throws If there is no entry with the given {@link id}.
|
||||
*/
|
||||
public get<P extends B>(id: number): P {
|
||||
const provider = this.map.get(id) as P
|
||||
if (provider === undefined) {
|
||||
throw new Error(`no provider with ID ${id}`)
|
||||
}
|
||||
return provider
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes the subscription that was previously assigned the given {@link id}, and removes it from the
|
||||
* map.
|
||||
*/
|
||||
public remove(id: number): void {
|
||||
if (!this.map.has(id)) {
|
||||
throw new Error(`no provider with ID ${id}`)
|
||||
}
|
||||
try {
|
||||
this.unsubscribeProvider(id)
|
||||
} finally {
|
||||
this.map.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes all subscriptions in this map and clears it.
|
||||
*/
|
||||
public unsubscribe(): void {
|
||||
try {
|
||||
for (const id of this.map.keys()) {
|
||||
this.unsubscribeProvider(id)
|
||||
}
|
||||
} finally {
|
||||
this.map.clear()
|
||||
}
|
||||
}
|
||||
export interface ProxySubscribable<T> extends ProxyValue {
|
||||
subscribe(observer: ProxyResult<Observer<T> & ProxyValue>): Unsubscribable & ProxyValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an Observable that emits the provider result.
|
||||
* Wraps a given Subscribable so that it is exposed by comlink to the other thread.
|
||||
*
|
||||
* @param subscribable A normal Subscribable (from this thread)
|
||||
*/
|
||||
export function toProviderResultObservable<T, R>(
|
||||
result: Promise<T | undefined | null | Subscribable<T | undefined | null>>,
|
||||
mapFunc: (value: T | undefined | null) => R | undefined | null
|
||||
): Observable<R | undefined | null> {
|
||||
return new Observable<R | undefined | null>(observer => {
|
||||
result
|
||||
.then(result => {
|
||||
let observable: Observable<R | undefined | null>
|
||||
if (result && (isPromise(result) || isObservable(result) || isSubscribable(result))) {
|
||||
observable = from(result as Promise<any> | RxJSSubscribable<any>).pipe(map(mapFunc))
|
||||
} else {
|
||||
observable = of(mapFunc(result))
|
||||
}
|
||||
observable.subscribe(observer)
|
||||
export const proxySubscribable = <T>(subscribable: Subscribable<T>): ProxySubscribable<T> => ({
|
||||
[proxyValueSymbol]: true,
|
||||
subscribe(observer): Unsubscribable & ProxyValue {
|
||||
return proxyValue(
|
||||
// Don't pass the proxy to Rx directly because it will try to
|
||||
// access Symbol properties that cannot be proxied
|
||||
subscribable.subscribe({
|
||||
next: val => {
|
||||
// tslint:disable-next-line: no-floating-promises
|
||||
observer.next(val as UnproxyOrClone<T>)
|
||||
},
|
||||
error: err => {
|
||||
// tslint:disable-next-line: no-floating-promises
|
||||
observer.error(err)
|
||||
},
|
||||
complete: () => {
|
||||
// tslint:disable-next-line: no-floating-promises
|
||||
observer.complete()
|
||||
},
|
||||
})
|
||||
.catch(err => observer.error(err))
|
||||
})
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Returns a Subscribable that can be proxied by comlink.
|
||||
*
|
||||
* @param result The result returned by the provider
|
||||
* @param mapFunc A function to map the result into a value to be transmitted to the other thread
|
||||
*/
|
||||
export function toProxyableSubscribable<T, R>(
|
||||
result: ProviderResult<T>,
|
||||
mapFunc: (value: T | undefined | null) => R
|
||||
): ProxySubscribable<R> {
|
||||
let observable: Observable<R>
|
||||
if (result && (isPromise(result) || isObservable<T>(result) || isSubscribable(result))) {
|
||||
observable = from(result).pipe(map(mapFunc))
|
||||
} else {
|
||||
observable = of(mapFunc(result))
|
||||
}
|
||||
return proxySubscribable(observable)
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { ProxyResult, ProxyValue, proxyValueSymbol } from '@sourcegraph/comlink'
|
||||
import { BehaviorSubject, PartialObserver, Unsubscribable } from 'rxjs'
|
||||
import * as sourcegraph from 'sourcegraph'
|
||||
import { SettingsCascade } from '../../../settings/settings'
|
||||
@ -8,7 +9,7 @@ import { ClientConfigurationAPI } from '../../client/api/configuration'
|
||||
* @template C - The configuration schema.
|
||||
*/
|
||||
class ExtConfigurationSection<C extends object> implements sourcegraph.Configuration<C> {
|
||||
constructor(private proxy: ClientConfigurationAPI, private data: C) {}
|
||||
constructor(private proxy: ProxyResult<ClientConfigurationAPI>, private data: C) {}
|
||||
|
||||
public get<K extends keyof C>(key: K): C[K] | undefined {
|
||||
return this.data[key]
|
||||
@ -34,24 +35,26 @@ class ExtConfigurationSection<C extends object> implements sourcegraph.Configura
|
||||
* @internal
|
||||
* @template C - The configuration schema.
|
||||
*/
|
||||
export interface ExtConfigurationAPI<C> {
|
||||
$acceptConfigurationData(data: Readonly<SettingsCascade<C>>): Promise<void>
|
||||
export interface ExtConfigurationAPI<C> extends ProxyValue {
|
||||
$acceptConfigurationData(data: Readonly<SettingsCascade<C>>): void
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @template C - The configuration schema.
|
||||
*/
|
||||
export class ExtConfiguration<C extends object> implements ExtConfigurationAPI<C> {
|
||||
export class ExtConfiguration<C extends object> implements ExtConfigurationAPI<C>, ProxyValue {
|
||||
public readonly [proxyValueSymbol] = true
|
||||
|
||||
/**
|
||||
* The settings data observable, assigned when the initial data is received from the client. Extensions should
|
||||
* never be able to call {@link ExtConfiguration}'s methods before the initial data is received.
|
||||
*/
|
||||
private data?: BehaviorSubject<Readonly<SettingsCascade<C>>>
|
||||
|
||||
constructor(private proxy: ClientConfigurationAPI) {}
|
||||
constructor(private proxy: ProxyResult<ClientConfigurationAPI>) {}
|
||||
|
||||
public async $acceptConfigurationData(data: Readonly<SettingsCascade<C>>): Promise<void> {
|
||||
public $acceptConfigurationData(data: Readonly<SettingsCascade<C>>): void {
|
||||
if (!this.data) {
|
||||
this.data = new BehaviorSubject(data)
|
||||
} else {
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { ProxyResult } from '@sourcegraph/comlink'
|
||||
import { ContextValues } from 'sourcegraph'
|
||||
import { ClientContextAPI } from '../../client/api/context'
|
||||
|
||||
/** @internal */
|
||||
export class ExtContext {
|
||||
constructor(private proxy: ClientContextAPI) {}
|
||||
constructor(private proxy: ProxyResult<ClientContextAPI>) {}
|
||||
|
||||
public updateContext(updates: ContextValues): void {
|
||||
// tslint:disable-next-line: no-floating-promises
|
||||
this.proxy.$acceptContextUpdates(updates)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
import { ProxyValue, proxyValueSymbol } from '@sourcegraph/comlink'
|
||||
import { Subject } from 'rxjs'
|
||||
import { TextDocument } from 'sourcegraph'
|
||||
import { TextDocumentItem } from '../../client/types/textDocument'
|
||||
|
||||
/** @internal */
|
||||
export interface ExtDocumentsAPI {
|
||||
export interface ExtDocumentsAPI extends ProxyValue {
|
||||
$acceptDocumentData(doc: TextDocumentItem[]): void
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ExtDocuments implements ExtDocumentsAPI {
|
||||
export class ExtDocuments implements ExtDocumentsAPI, ProxyValue {
|
||||
public readonly [proxyValueSymbol] = true
|
||||
|
||||
private documents = new Map<string, TextDocumentItem>()
|
||||
|
||||
constructor(private sync: () => Promise<void>) {}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { ProxyValue, proxyValueSymbol } from '@sourcegraph/comlink'
|
||||
import { Subscription, Unsubscribable } from 'rxjs'
|
||||
import { asError } from '../../../util/errors'
|
||||
import { tryCatchPromise } from '../../util'
|
||||
|
||||
/** @internal */
|
||||
export interface ExtExtensionsAPI {
|
||||
export interface ExtExtensionsAPI extends ProxyValue {
|
||||
$activateExtension(extensionID: string, bundleURL: string): Promise<void>
|
||||
$deactivateExtension(extensionID: string): Promise<void>
|
||||
}
|
||||
@ -12,7 +13,9 @@ export interface ExtExtensionsAPI {
|
||||
declare const self: any
|
||||
|
||||
/** @internal */
|
||||
export class ExtExtensions implements ExtExtensionsAPI, Unsubscribable {
|
||||
export class ExtExtensions implements ExtExtensionsAPI, Unsubscribable, ProxyValue {
|
||||
public readonly [proxyValueSymbol] = true
|
||||
|
||||
/** Extensions' deactivate functions. */
|
||||
private extensionDeactivate = new Map<string, (() => void | Promise<void>)>()
|
||||
|
||||
|
||||
@ -1,202 +1,100 @@
|
||||
import { ProxyInput, ProxyResult, proxyValue } from '@sourcegraph/comlink'
|
||||
import * as clientType from '@sourcegraph/extension-api-types'
|
||||
import { Observable, Unsubscribable } from 'rxjs'
|
||||
import { Unsubscribable } from 'rxjs'
|
||||
import {
|
||||
DefinitionProvider,
|
||||
DocumentSelector,
|
||||
Hover,
|
||||
HoverProvider,
|
||||
ImplementationProvider,
|
||||
Location,
|
||||
LocationProvider,
|
||||
ReferenceContext,
|
||||
ReferenceProvider,
|
||||
Subscribable,
|
||||
TypeDefinitionProvider,
|
||||
} from 'sourcegraph'
|
||||
import { ClientLanguageFeaturesAPI } from '../../client/api/languageFeatures'
|
||||
import { ProviderMap, toProviderResultObservable } from './common'
|
||||
import { ReferenceParams, TextDocumentPositionParams } from '../../protocol'
|
||||
import { syncSubscription } from '../../util'
|
||||
import { toProxyableSubscribable } from './common'
|
||||
import { ExtDocuments } from './documents'
|
||||
import { fromHover, fromLocation, toPosition } from './types'
|
||||
|
||||
/** @internal */
|
||||
export interface ExtLanguageFeaturesAPI {
|
||||
$observeHover(
|
||||
id: number,
|
||||
resource: string,
|
||||
position: clientType.Position
|
||||
): Observable<clientType.Hover | null | undefined>
|
||||
$observeDefinition(
|
||||
id: number,
|
||||
resource: string,
|
||||
position: clientType.Position
|
||||
): Observable<clientType.Location[] | null | undefined>
|
||||
$observeTypeDefinition(
|
||||
id: number,
|
||||
resource: string,
|
||||
position: clientType.Position
|
||||
): Observable<clientType.Location[] | null | undefined>
|
||||
$observeImplementation(
|
||||
id: number,
|
||||
resource: string,
|
||||
position: clientType.Position
|
||||
): Observable<clientType.Location[] | null | undefined>
|
||||
$observeReferences(
|
||||
id: number,
|
||||
resource: string,
|
||||
position: clientType.Position,
|
||||
context: ReferenceContext
|
||||
): Observable<clientType.Location[] | null | undefined>
|
||||
$observeLocations(
|
||||
id: number,
|
||||
resource: string,
|
||||
position: clientType.Position
|
||||
): Observable<clientType.Location[] | null | undefined>
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ExtLanguageFeatures implements ExtLanguageFeaturesAPI, Unsubscribable {
|
||||
private registrations = new ProviderMap<
|
||||
| HoverProvider
|
||||
| DefinitionProvider
|
||||
| TypeDefinitionProvider
|
||||
| ImplementationProvider
|
||||
| ReferenceProvider
|
||||
| LocationProvider
|
||||
>(id => this.proxy.$unregister(id))
|
||||
|
||||
constructor(private proxy: ClientLanguageFeaturesAPI, private documents: ExtDocuments) {}
|
||||
|
||||
public $observeHover(
|
||||
id: number,
|
||||
resource: string,
|
||||
position: clientType.Position
|
||||
): Observable<clientType.Hover | null | undefined> {
|
||||
const provider = this.registrations.get<HoverProvider>(id)
|
||||
return toProviderResultObservable(
|
||||
this.documents
|
||||
.getSync(resource)
|
||||
.then<Hover | undefined | null | Subscribable<Hover | undefined | null>>(document =>
|
||||
provider.provideHover(document, toPosition(position))
|
||||
),
|
||||
hover => (hover ? fromHover(hover) : hover)
|
||||
)
|
||||
}
|
||||
export class ExtLanguageFeatures {
|
||||
constructor(private proxy: ProxyResult<ClientLanguageFeaturesAPI>, private documents: ExtDocuments) {}
|
||||
|
||||
public registerHoverProvider(selector: DocumentSelector, provider: HoverProvider): Unsubscribable {
|
||||
const { id, subscription } = this.registrations.add(provider)
|
||||
this.proxy.$registerHoverProvider(id, selector)
|
||||
return subscription
|
||||
}
|
||||
|
||||
public $observeDefinition(
|
||||
id: number,
|
||||
resource: string,
|
||||
position: clientType.Position
|
||||
): Observable<clientType.Location[] | null | undefined> {
|
||||
const provider = this.registrations.get<DefinitionProvider>(id)
|
||||
return toProviderResultObservable(
|
||||
this.documents
|
||||
.getSync(resource)
|
||||
.then<
|
||||
Location | Location[] | null | undefined | Subscribable<Location | Location[] | null | undefined>
|
||||
>(document => provider.provideDefinition(document, toPosition(position))),
|
||||
toDefinition
|
||||
const providerFunction: ProxyInput<
|
||||
Parameters<ClientLanguageFeaturesAPI['$registerHoverProvider']>[1]
|
||||
> = proxyValue(async ({ textDocument, position }: TextDocumentPositionParams) =>
|
||||
toProxyableSubscribable(
|
||||
provider.provideHover(await this.documents.getSync(textDocument.uri), toPosition(position)),
|
||||
hover => (hover ? fromHover(hover) : hover)
|
||||
)
|
||||
)
|
||||
return syncSubscription(this.proxy.$registerHoverProvider(selector, providerFunction))
|
||||
}
|
||||
|
||||
public registerDefinitionProvider(selector: DocumentSelector, provider: DefinitionProvider): Unsubscribable {
|
||||
const { id, subscription } = this.registrations.add(provider)
|
||||
this.proxy.$registerDefinitionProvider(id, selector)
|
||||
return subscription
|
||||
}
|
||||
|
||||
public $observeTypeDefinition(
|
||||
id: number,
|
||||
resource: string,
|
||||
position: clientType.Position
|
||||
): Observable<clientType.Location[] | null | undefined> {
|
||||
const provider = this.registrations.get<TypeDefinitionProvider>(id)
|
||||
return toProviderResultObservable(
|
||||
this.documents
|
||||
.getSync(resource)
|
||||
.then<
|
||||
Location | Location[] | null | undefined | Subscribable<Location | Location[] | null | undefined>
|
||||
>(document => provider.provideTypeDefinition(document, toPosition(position))),
|
||||
toDefinition
|
||||
const providerFunction: ProxyInput<
|
||||
Parameters<ClientLanguageFeaturesAPI['$registerDefinitionProvider']>[1]
|
||||
> = proxyValue(async ({ textDocument, position }: TextDocumentPositionParams) =>
|
||||
toProxyableSubscribable(
|
||||
provider.provideDefinition(await this.documents.getSync(textDocument.uri), toPosition(position)),
|
||||
toLocations
|
||||
)
|
||||
)
|
||||
return syncSubscription(this.proxy.$registerDefinitionProvider(selector, providerFunction))
|
||||
}
|
||||
|
||||
public registerTypeDefinitionProvider(
|
||||
selector: DocumentSelector,
|
||||
provider: TypeDefinitionProvider
|
||||
): Unsubscribable {
|
||||
const { id, subscription } = this.registrations.add(provider)
|
||||
this.proxy.$registerTypeDefinitionProvider(id, selector)
|
||||
return subscription
|
||||
}
|
||||
|
||||
public $observeImplementation(
|
||||
id: number,
|
||||
resource: string,
|
||||
position: clientType.Position
|
||||
): Observable<clientType.Location[] | null | undefined> {
|
||||
const provider = this.registrations.get<ImplementationProvider>(id)
|
||||
return toProviderResultObservable(
|
||||
this.documents
|
||||
.getSync(resource)
|
||||
.then<
|
||||
Location | Location[] | null | undefined | Subscribable<Location | Location[] | null | undefined>
|
||||
>(document => provider.provideImplementation(document, toPosition(position))),
|
||||
toDefinition
|
||||
const providerFunction: ProxyInput<
|
||||
Parameters<ClientLanguageFeaturesAPI['$registerTypeDefinitionProvider']>[1]
|
||||
> = proxyValue(async ({ textDocument, position }: TextDocumentPositionParams) =>
|
||||
toProxyableSubscribable(
|
||||
provider.provideTypeDefinition(await this.documents.getSync(textDocument.uri), toPosition(position)),
|
||||
toLocations
|
||||
)
|
||||
)
|
||||
return syncSubscription(this.proxy.$registerTypeDefinitionProvider(selector, providerFunction))
|
||||
}
|
||||
|
||||
public registerImplementationProvider(
|
||||
selector: DocumentSelector,
|
||||
provider: ImplementationProvider
|
||||
): Unsubscribable {
|
||||
const { id, subscription } = this.registrations.add(provider)
|
||||
this.proxy.$registerImplementationProvider(id, selector)
|
||||
return subscription
|
||||
}
|
||||
|
||||
public $observeReferences(
|
||||
id: number,
|
||||
resource: string,
|
||||
position: clientType.Position,
|
||||
context: ReferenceContext
|
||||
): Observable<clientType.Location[] | null | undefined> {
|
||||
const provider = this.registrations.get<ReferenceProvider>(id)
|
||||
return toProviderResultObservable(
|
||||
this.documents
|
||||
.getSync(resource)
|
||||
.then<Location[] | null | undefined | Subscribable<Location[] | null | undefined>>(document =>
|
||||
provider.provideReferences(document, toPosition(position), context)
|
||||
),
|
||||
toLocations
|
||||
return syncSubscription(
|
||||
this.proxy.$registerImplementationProvider(
|
||||
selector,
|
||||
proxyValue(async ({ textDocument, position }: TextDocumentPositionParams) =>
|
||||
toProxyableSubscribable(
|
||||
provider.provideImplementation(
|
||||
await this.documents.getSync(textDocument.uri),
|
||||
toPosition(position)
|
||||
),
|
||||
toLocations
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
public registerReferenceProvider(selector: DocumentSelector, provider: ReferenceProvider): Unsubscribable {
|
||||
const { id, subscription } = this.registrations.add(provider)
|
||||
this.proxy.$registerReferenceProvider(id, selector)
|
||||
return subscription
|
||||
}
|
||||
|
||||
public $observeLocations(
|
||||
id: number,
|
||||
resource: string,
|
||||
position: clientType.Position
|
||||
): Observable<clientType.Location[] | null | undefined> {
|
||||
const provider = this.registrations.get<LocationProvider>(id)
|
||||
return toProviderResultObservable(
|
||||
this.documents
|
||||
.getSync(resource)
|
||||
.then<Location[] | null | undefined | Subscribable<Location[] | null | undefined>>(document =>
|
||||
provider.provideLocations(document, toPosition(position))
|
||||
const providerFunction: ProxyInput<
|
||||
Parameters<ClientLanguageFeaturesAPI['$registerReferenceProvider']>[1]
|
||||
> = proxyValue(async ({ textDocument, position, context }: ReferenceParams) =>
|
||||
toProxyableSubscribable(
|
||||
provider.provideReferences(
|
||||
await this.documents.getSync(textDocument.uri),
|
||||
toPosition(position),
|
||||
context
|
||||
),
|
||||
toLocations
|
||||
toLocations
|
||||
)
|
||||
)
|
||||
return syncSubscription(this.proxy.$registerReferenceProvider(selector, providerFunction))
|
||||
}
|
||||
|
||||
public registerLocationProvider(
|
||||
@ -204,25 +102,18 @@ export class ExtLanguageFeatures implements ExtLanguageFeaturesAPI, Unsubscribab
|
||||
selector: DocumentSelector,
|
||||
provider: LocationProvider
|
||||
): Unsubscribable {
|
||||
/**
|
||||
* {@link idStr} is the `id` parameter to {@link sourcegraph.languages.registerLocationProvider} that
|
||||
* identifies the provider and is chosen by the extension. {@link id} is an internal implementation detail:
|
||||
* the numeric registry ID used to identify this provider solely between the client and extension host.
|
||||
*/
|
||||
const { id, subscription } = this.registrations.add(provider)
|
||||
this.proxy.$registerLocationProvider(id, idStr, selector)
|
||||
return subscription
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
this.registrations.unsubscribe()
|
||||
const providerFunction: ProxyInput<
|
||||
Parameters<ClientLanguageFeaturesAPI['$registerLocationProvider']>[2]
|
||||
> = proxyValue(async ({ textDocument, position }: TextDocumentPositionParams) =>
|
||||
toProxyableSubscribable(
|
||||
provider.provideLocations(await this.documents.getSync(textDocument.uri), toPosition(position)),
|
||||
toLocations
|
||||
)
|
||||
)
|
||||
return syncSubscription(this.proxy.$registerLocationProvider(idStr, selector, proxyValue(providerFunction)))
|
||||
}
|
||||
}
|
||||
|
||||
function toLocations(result: Location[] | null | undefined): clientType.Location[] | null | undefined {
|
||||
return result ? result.map(location => fromLocation(location)) : result
|
||||
}
|
||||
|
||||
function toDefinition(result: Location[] | Location | null | undefined): clientType.Location[] | null | undefined {
|
||||
return result ? (Array.isArray(result) ? result : [result]).map(location => fromLocation(location)) : result
|
||||
function toLocations(result: Location[] | Location | null | undefined): clientType.Location[] {
|
||||
return result ? (Array.isArray(result) ? result : [result]).map(location => fromLocation(location)) : []
|
||||
}
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
import { ProxyValue, proxyValueSymbol } from '@sourcegraph/comlink'
|
||||
import * as clientType from '@sourcegraph/extension-api-types'
|
||||
import { Subject } from 'rxjs'
|
||||
import * as sourcegraph from 'sourcegraph'
|
||||
import { URI } from '../types/uri'
|
||||
|
||||
/** @internal */
|
||||
export interface ExtRootsAPI {
|
||||
export interface ExtRootsAPI extends ProxyValue {
|
||||
$acceptRoots(roots: clientType.WorkspaceRoot[]): void
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ExtRoots implements ExtRootsAPI {
|
||||
export class ExtRoots implements ExtRootsAPI, ProxyValue {
|
||||
public readonly [proxyValueSymbol] = true
|
||||
|
||||
private roots: ReadonlyArray<sourcegraph.WorkspaceRoot> = []
|
||||
|
||||
/**
|
||||
@ -24,9 +27,7 @@ export class ExtRoots implements ExtRootsAPI {
|
||||
public readonly changes = new Subject<void>()
|
||||
|
||||
public $acceptRoots(roots: clientType.WorkspaceRoot[]): void {
|
||||
this.roots = Object.freeze(
|
||||
roots.map(plain => ({ ...plain, uri: new URI(plain.uri) } as sourcegraph.WorkspaceRoot))
|
||||
)
|
||||
this.roots = Object.freeze(roots.map(plain => ({ ...plain, uri: new URI(plain.uri) })))
|
||||
this.changes.next()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,28 +1,13 @@
|
||||
import { ProxyResult, proxyValue } from '@sourcegraph/comlink'
|
||||
import { Unsubscribable } from 'rxjs'
|
||||
import { QueryTransformer } from 'sourcegraph'
|
||||
import { SearchAPI } from '../../client/api/search'
|
||||
import { ProviderMap } from './common'
|
||||
import { ClientSearchAPI } from '../../client/api/search'
|
||||
import { syncSubscription } from '../../util'
|
||||
|
||||
export interface ExtSearchAPI {
|
||||
$transformQuery: (id: number, query: string) => Promise<string>
|
||||
}
|
||||
|
||||
export class ExtSearch implements ExtSearchAPI, Unsubscribable {
|
||||
private registrations = new ProviderMap<QueryTransformer>(id => this.proxy.$unregister(id))
|
||||
constructor(private proxy: SearchAPI) {}
|
||||
export class ExtSearch {
|
||||
constructor(private proxy: ProxyResult<ClientSearchAPI>) {}
|
||||
|
||||
public registerQueryTransformer(provider: QueryTransformer): Unsubscribable {
|
||||
const { id, subscription } = this.registrations.add(provider)
|
||||
this.proxy.$registerQueryTransformer(id)
|
||||
return subscription
|
||||
}
|
||||
|
||||
public $transformQuery(id: number, query: string): Promise<string> {
|
||||
const provider = this.registrations.get<QueryTransformer>(id)
|
||||
return Promise.resolve(provider.transformQuery(query))
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
this.registrations.unsubscribe()
|
||||
return syncSubscription(this.proxy.$registerQueryTransformer(proxyValue(provider)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { Unsubscribable } from 'rxjs'
|
||||
import { ProxyResult, ProxyValue, proxyValueSymbol } from '@sourcegraph/comlink'
|
||||
import * as sourcegraph from 'sourcegraph'
|
||||
import { ClientViewsAPI, PanelViewData } from '../../client/api/views'
|
||||
import { ProviderMap } from './common'
|
||||
import { ClientViewsAPI, PanelUpdater, PanelViewData } from '../../client/api/views'
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -14,7 +13,7 @@ class ExtPanelView implements sourcegraph.PanelView {
|
||||
component: null,
|
||||
}
|
||||
|
||||
constructor(private proxy: ClientViewsAPI, private id: number, private subscription: Unsubscribable) {}
|
||||
constructor(private proxyPromise: Promise<ProxyResult<PanelUpdater>>) {}
|
||||
|
||||
public get title(): string {
|
||||
return this.data.title
|
||||
@ -49,27 +48,24 @@ class ExtPanelView implements sourcegraph.PanelView {
|
||||
}
|
||||
|
||||
private sendData(): void {
|
||||
this.proxy.$acceptPanelViewUpdate(this.id, this.data)
|
||||
// tslint:disable-next-line: no-floating-promises
|
||||
this.proxyPromise.then(proxy => proxy.update(this.data))
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
return this.subscription.unsubscribe()
|
||||
// tslint:disable-next-line: no-floating-promises
|
||||
this.proxyPromise.then(proxy => proxy.unsubscribe())
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ExtViews implements Unsubscribable {
|
||||
private registrations = new ProviderMap<{}>(id => this.proxy.$unregister(id))
|
||||
export class ExtViews implements ProxyValue {
|
||||
public readonly [proxyValueSymbol] = true
|
||||
|
||||
constructor(private proxy: ClientViewsAPI) {}
|
||||
constructor(private proxy: ProxyResult<ClientViewsAPI>) {}
|
||||
|
||||
public createPanelView(id: string): ExtPanelView {
|
||||
const { id: regID, subscription } = this.registrations.add({})
|
||||
this.proxy.$registerPanelViewProvider(regID, { id })
|
||||
return new ExtPanelView(this.proxy, regID, subscription)
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
this.registrations.unsubscribe()
|
||||
const panelProxyPromise = this.proxy.$registerPanelViewProvider({ id })
|
||||
return new ExtPanelView(panelProxyPromise)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { BehaviorSubject, Observer, of } from 'rxjs'
|
||||
import { ProxyResult, ProxyValue, proxyValueSymbol } from '@sourcegraph/comlink'
|
||||
import { BehaviorSubject, of } from 'rxjs'
|
||||
import * as sourcegraph from 'sourcegraph'
|
||||
import { asError } from '../../../util/errors'
|
||||
import { ClientCodeEditorAPI } from '../../client/api/codeEditor'
|
||||
@ -17,7 +18,7 @@ export interface WindowData {
|
||||
* @internal
|
||||
*/
|
||||
class ExtWindow implements sourcegraph.Window {
|
||||
constructor(private windowsProxy: ClientWindowsAPI, private readonly textEditors: ExtCodeEditor[]) {}
|
||||
constructor(private windowsProxy: ProxyResult<ClientWindowsAPI>, private readonly textEditors: ExtCodeEditor[]) {}
|
||||
|
||||
public readonly activeViewComponentChanges = of(this.activeViewComponent)
|
||||
|
||||
@ -30,6 +31,7 @@ class ExtWindow implements sourcegraph.Window {
|
||||
}
|
||||
|
||||
public showNotification(message: string): void {
|
||||
// tslint:disable-next-line: no-floating-promises
|
||||
this.windowsProxy.$showNotification(message)
|
||||
}
|
||||
|
||||
@ -57,23 +59,26 @@ class ExtWindow implements sourcegraph.Window {
|
||||
}
|
||||
|
||||
public async showProgress(options: sourcegraph.ProgressOptions): Promise<sourcegraph.ProgressReporter> {
|
||||
const handle = await this.windowsProxy.$startProgress(options)
|
||||
const reporter: Observer<sourcegraph.Progress> = {
|
||||
next: (progress: sourcegraph.Progress): void => {
|
||||
this.windowsProxy.$updateProgress(handle, progress)
|
||||
const reporterProxy = await this.windowsProxy.$showProgress(options)
|
||||
return {
|
||||
next: progress => {
|
||||
// tslint:disable-next-line: no-floating-promises
|
||||
reporterProxy.next(progress)
|
||||
},
|
||||
error: (err: any): void => {
|
||||
error: err => {
|
||||
const error = asError(err)
|
||||
this.windowsProxy.$updateProgress(handle, undefined, {
|
||||
// tslint:disable-next-line: no-floating-promises
|
||||
reporterProxy.error({
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
stack: error.stack,
|
||||
})
|
||||
},
|
||||
complete: (): void => {
|
||||
this.windowsProxy.$updateProgress(handle, undefined, undefined, true)
|
||||
complete: () => {
|
||||
// tslint:disable-next-line: no-floating-promises
|
||||
reporterProxy.complete()
|
||||
},
|
||||
}
|
||||
return reporter
|
||||
}
|
||||
|
||||
public toJSON(): any {
|
||||
@ -82,18 +87,19 @@ class ExtWindow implements sourcegraph.Window {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface ExtWindowsAPI {
|
||||
export interface ExtWindowsAPI extends ProxyValue {
|
||||
$acceptWindowData(allWindows: WindowData[]): void
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ExtWindows implements ExtWindowsAPI {
|
||||
export class ExtWindows implements ExtWindowsAPI, ProxyValue {
|
||||
public readonly [proxyValueSymbol] = true
|
||||
|
||||
private data: WindowData[] = []
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
private windowsProxy: ClientWindowsAPI,
|
||||
private codeEditorProxy: ClientCodeEditorAPI,
|
||||
private proxy: ProxyResult<{ windows: ClientWindowsAPI; codeEditor: ClientCodeEditorAPI }>,
|
||||
private documents: ExtDocuments
|
||||
) {}
|
||||
|
||||
@ -113,14 +119,14 @@ export class ExtWindows implements ExtWindowsAPI {
|
||||
return this.data.map(
|
||||
window =>
|
||||
new ExtWindow(
|
||||
this.windowsProxy,
|
||||
this.proxy.windows,
|
||||
window.visibleViewComponents.map(
|
||||
c =>
|
||||
new ExtCodeEditor(
|
||||
c.item.uri,
|
||||
c.selections,
|
||||
c.isActive,
|
||||
this.codeEditorProxy,
|
||||
this.proxy.codeEditor,
|
||||
this.documents
|
||||
)
|
||||
)
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import * as comlink from '@sourcegraph/comlink'
|
||||
import { Subscription, Unsubscribable } from 'rxjs'
|
||||
import * as sourcegraph from 'sourcegraph'
|
||||
import { createProxy, handleRequests } from '../common/proxy'
|
||||
import { Connection, createConnection, Logger, MessageTransports } from '../protocol/jsonrpc2/connection'
|
||||
import { EndpointPair } from '../../platform/context'
|
||||
import { ClientAPI } from '../client/api/api'
|
||||
import { ExtensionHostAPI, ExtensionHostAPIFactory } from './api/api'
|
||||
import { ExtCommands } from './api/commands'
|
||||
import { ExtConfiguration } from './api/configuration'
|
||||
import { ExtContext } from './api/context'
|
||||
@ -19,8 +21,6 @@ import { Range } from './types/range'
|
||||
import { Selection } from './types/selection'
|
||||
import { URI } from './types/uri'
|
||||
|
||||
const consoleLogger: Logger = console
|
||||
|
||||
/**
|
||||
* Required information when initializing an extension host.
|
||||
*/
|
||||
@ -44,30 +44,31 @@ export interface InitData {
|
||||
* @return An unsubscribable to terminate the extension host.
|
||||
*/
|
||||
export function startExtensionHost(
|
||||
transports: MessageTransports
|
||||
): Unsubscribable & { __testAPI: Promise<typeof sourcegraph> } {
|
||||
const connection = createConnection(transports, consoleLogger)
|
||||
connection.listen()
|
||||
|
||||
endpoints: EndpointPair
|
||||
): Unsubscribable & { extensionAPI: Promise<typeof sourcegraph> } {
|
||||
const subscription = new Subscription()
|
||||
subscription.add(connection)
|
||||
|
||||
// Wait for "initialize" message from client application before proceeding to create the
|
||||
// extension host.
|
||||
let initialized = false
|
||||
const __testAPI = new Promise<typeof sourcegraph>(resolve => {
|
||||
connection.onRequest('initialize', ([initData]: [InitData]) => {
|
||||
const extensionAPI = new Promise<typeof sourcegraph>(resolve => {
|
||||
const factory: ExtensionHostAPIFactory = initData => {
|
||||
if (initialized) {
|
||||
throw new Error('extension host is already initialized')
|
||||
}
|
||||
initialized = true
|
||||
const { unsubscribe, __testAPI } = initializeExtensionHost(connection, initData)
|
||||
subscription.add(unsubscribe)
|
||||
resolve(__testAPI)
|
||||
})
|
||||
const { subscription: extHostSubscription, extensionAPI, extensionHostAPI } = initializeExtensionHost(
|
||||
endpoints,
|
||||
initData
|
||||
)
|
||||
subscription.add(extHostSubscription)
|
||||
resolve(extensionAPI)
|
||||
return extensionHostAPI
|
||||
}
|
||||
comlink.expose(factory, endpoints.expose)
|
||||
})
|
||||
|
||||
return { unsubscribe: () => subscription.unsubscribe(), __testAPI }
|
||||
return { unsubscribe: () => subscription.unsubscribe(), extensionAPI }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -82,24 +83,24 @@ export function startExtensionHost(
|
||||
* @return An unsubscribable to terminate the extension host.
|
||||
*/
|
||||
function initializeExtensionHost(
|
||||
connection: Connection,
|
||||
endpoints: EndpointPair,
|
||||
initData: InitData
|
||||
): Unsubscribable & { __testAPI: typeof sourcegraph } {
|
||||
const subscriptions = new Subscription()
|
||||
): { extensionHostAPI: ExtensionHostAPI; extensionAPI: typeof sourcegraph; subscription: Subscription } {
|
||||
const subscription = new Subscription()
|
||||
|
||||
const { api, subscription: apiSubscription } = createExtensionAPI(initData, connection)
|
||||
subscriptions.add(apiSubscription)
|
||||
const { extensionAPI, extensionHostAPI, subscription: apiSubscription } = createExtensionAPI(initData, endpoints)
|
||||
subscription.add(apiSubscription)
|
||||
|
||||
// Make `import 'sourcegraph'` or `require('sourcegraph')` return the extension API.
|
||||
;(global as any).require = (modulePath: string): any => {
|
||||
if (modulePath === 'sourcegraph') {
|
||||
return api
|
||||
return extensionAPI
|
||||
}
|
||||
// All other requires/imports in the extension's code should not reach here because their JS
|
||||
// bundler should have resolved them locally.
|
||||
throw new Error(`require: module not found: ${modulePath}`)
|
||||
}
|
||||
subscriptions.add(() => {
|
||||
subscription.add(() => {
|
||||
;(global as any).require = () => {
|
||||
// Prevent callers from attempting to access the extension API after it was
|
||||
// unsubscribed.
|
||||
@ -107,55 +108,52 @@ function initializeExtensionHost(
|
||||
}
|
||||
})
|
||||
|
||||
return { unsubscribe: () => subscriptions.unsubscribe(), __testAPI: api }
|
||||
return { subscription, extensionAPI, extensionHostAPI }
|
||||
}
|
||||
|
||||
function createExtensionAPI(
|
||||
initData: InitData,
|
||||
connection: Connection
|
||||
): { api: typeof sourcegraph; subscription: Subscription } {
|
||||
const subscriptions = new Subscription()
|
||||
endpoints: Pick<EndpointPair, 'proxy'>
|
||||
): { extensionHostAPI: ExtensionHostAPI; extensionAPI: typeof sourcegraph; subscription: Subscription } {
|
||||
const subscription = new Subscription()
|
||||
|
||||
// EXTENSION HOST WORKER
|
||||
|
||||
/** Proxy to main thread */
|
||||
const proxy = comlink.proxy<ClientAPI>(endpoints.proxy)
|
||||
|
||||
// For debugging/tests.
|
||||
const sync = () => connection.sendRequest<void>('ping')
|
||||
connection.onRequest('ping', () => 'pong')
|
||||
|
||||
const context = new ExtContext(createProxy(connection, 'context'))
|
||||
handleRequests(connection, 'context', context)
|
||||
|
||||
const sync = async () => {
|
||||
await proxy.ping()
|
||||
}
|
||||
const context = new ExtContext(proxy.context)
|
||||
const documents = new ExtDocuments(sync)
|
||||
handleRequests(connection, 'documents', documents)
|
||||
|
||||
const extensions = new ExtExtensions()
|
||||
subscriptions.add(extensions)
|
||||
handleRequests(connection, 'extensions', extensions)
|
||||
subscription.add(extensions)
|
||||
|
||||
const roots = new ExtRoots()
|
||||
handleRequests(connection, 'roots', roots)
|
||||
const windows = new ExtWindows(proxy, documents)
|
||||
const views = new ExtViews(proxy.views)
|
||||
const configuration = new ExtConfiguration<any>(proxy.configuration)
|
||||
const languageFeatures = new ExtLanguageFeatures(proxy.languageFeatures, documents)
|
||||
const search = new ExtSearch(proxy.search)
|
||||
const commands = new ExtCommands(proxy.commands)
|
||||
|
||||
const windows = new ExtWindows(createProxy(connection, 'windows'), createProxy(connection, 'codeEditor'), documents)
|
||||
handleRequests(connection, 'windows', windows)
|
||||
// Expose the extension host API to the client (main thread)
|
||||
const extensionHostAPI: ExtensionHostAPI = {
|
||||
[comlink.proxyValueSymbol]: true,
|
||||
|
||||
const views = new ExtViews(createProxy(connection, 'views'))
|
||||
subscriptions.add(views)
|
||||
handleRequests(connection, 'views', views)
|
||||
ping: () => 'pong',
|
||||
configuration,
|
||||
documents,
|
||||
extensions,
|
||||
roots,
|
||||
windows,
|
||||
}
|
||||
|
||||
const configuration = new ExtConfiguration<any>(createProxy(connection, 'configuration'))
|
||||
handleRequests(connection, 'configuration', configuration)
|
||||
|
||||
const languageFeatures = new ExtLanguageFeatures(createProxy(connection, 'languageFeatures'), documents)
|
||||
subscriptions.add(languageFeatures)
|
||||
handleRequests(connection, 'languageFeatures', languageFeatures)
|
||||
|
||||
const search = new ExtSearch(createProxy(connection, 'search'))
|
||||
subscriptions.add(search)
|
||||
handleRequests(connection, 'search', search)
|
||||
|
||||
const commands = new ExtCommands(createProxy(connection, 'commands'))
|
||||
subscriptions.add(commands)
|
||||
handleRequests(connection, 'commands', commands)
|
||||
|
||||
const api: typeof sourcegraph = {
|
||||
// Expose the extension API to extensions
|
||||
const extensionAPI: typeof sourcegraph = {
|
||||
URI,
|
||||
Position,
|
||||
Range,
|
||||
@ -251,5 +249,5 @@ function createExtensionAPI(
|
||||
clientApplication: initData.clientApplication,
|
||||
},
|
||||
}
|
||||
return { api, subscription: subscriptions }
|
||||
return { extensionHostAPI, extensionAPI, subscription }
|
||||
}
|
||||
|
||||
@ -1,26 +1,64 @@
|
||||
// The ponyfill symbol-observable is impure. Since extensions are loaded through importScripts,
|
||||
// if one of our extensions depends on symbol-observable, it may break other extensions:
|
||||
// https://github.com/sourcegraph/sourcegraph/issues/1243
|
||||
// Importing symbol-observable when starting the web worker fixes this by
|
||||
// ensuring that `Symbol.observable` is mutated happens before any extensions are loaded.
|
||||
import 'symbol-observable'
|
||||
import { createWebWorkerMessageTransports } from '../protocol/jsonrpc2/transports/webWorker'
|
||||
import '../../polyfills'
|
||||
|
||||
import * as MessageChannelAdapter from '@sourcegraph/comlink/messagechanneladapter'
|
||||
import { fromEvent } from 'rxjs'
|
||||
import { take } from 'rxjs/operators'
|
||||
import { EndpointPair, isEndpointPair } from '../../platform/context'
|
||||
import { startExtensionHost } from './extensionHost'
|
||||
|
||||
export interface InitMessage {
|
||||
endpoints: {
|
||||
proxy: MessagePort
|
||||
expose: MessagePort
|
||||
}
|
||||
/**
|
||||
* Whether the endpoints should be wrapped with a comlink {@link MessageChannelAdapter}.
|
||||
*
|
||||
* This is true when the messages passed on the endpoints are forwarded to/from
|
||||
* other wrapped endpoints, like in the browser extension.
|
||||
*/
|
||||
wrapEndpoints: boolean
|
||||
}
|
||||
|
||||
const isInitMessage = (value: any): value is InitMessage => value.endpoints && isEndpointPair(value.endpoints)
|
||||
|
||||
const wrapMessagePort = (port: MessagePort) =>
|
||||
MessageChannelAdapter.wrap({
|
||||
send: data => port.postMessage(data),
|
||||
addEventListener: (event, listener) => port.addEventListener(event, listener),
|
||||
removeEventListener: (event, listener) => port.removeEventListener(event, listener),
|
||||
})
|
||||
|
||||
const wrapEndpoints = ({ proxy, expose }: InitMessage['endpoints']): EndpointPair => {
|
||||
proxy.start()
|
||||
expose.start()
|
||||
return {
|
||||
proxy: wrapMessagePort(proxy),
|
||||
expose: wrapMessagePort(expose),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The entrypoint for the JavaScript context that runs the extension host (and all extensions).
|
||||
*
|
||||
* To initialize the extension host, the parent sends it an "initialize" message with
|
||||
* {@link InitData}.
|
||||
* To initialize the extension host, the parent sends it an {@link InitMessage}
|
||||
*/
|
||||
function extensionHostMain(): void {
|
||||
async function extensionHostMain(): Promise<void> {
|
||||
try {
|
||||
const { unsubscribe } = startExtensionHost(createWebWorkerMessageTransports())
|
||||
self.addEventListener('unload', () => unsubscribe())
|
||||
const event = await fromEvent<MessageEvent>(self, 'message')
|
||||
.pipe(take(1))
|
||||
.toPromise()
|
||||
if (!isInitMessage(event.data)) {
|
||||
throw new Error('First message event in extension host worker was not a well-formed InitMessage')
|
||||
}
|
||||
const { endpoints } = event.data
|
||||
const extensionHost = startExtensionHost(event.data.wrapEndpoints ? wrapEndpoints(endpoints) : endpoints)
|
||||
self.addEventListener('unload', () => extensionHost.unsubscribe())
|
||||
} catch (err) {
|
||||
console.error('Error starting the extension host:', err)
|
||||
self.close()
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: no-floating-promises
|
||||
extensionHostMain()
|
||||
|
||||
39
shared/src/api/extension/worker.ts
Normal file
39
shared/src/api/extension/worker.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Observable } from 'rxjs'
|
||||
import ExtensionHostWorker from 'worker-loader?inline!./main.worker.ts'
|
||||
import { EndpointPair } from '../../platform/context'
|
||||
|
||||
interface ExtensionHostInitOptions {
|
||||
/**
|
||||
* Whether the endpoints should be wrapped with a comlink {@link MessageChannelAdapter}.
|
||||
*
|
||||
* This is true when the messages passed on the endpoints are forwarded to/from
|
||||
* other wrapped endpoints, like in the browser extension.
|
||||
*/
|
||||
wrapEndpoints: boolean
|
||||
}
|
||||
|
||||
export function createExtensionHostWorker({
|
||||
wrapEndpoints,
|
||||
}: ExtensionHostInitOptions): { worker: ExtensionHostWorker; clientEndpoints: EndpointPair } {
|
||||
const clientAPIChannel = new MessageChannel()
|
||||
const extensionHostAPIChannel = new MessageChannel()
|
||||
const worker = new ExtensionHostWorker()
|
||||
const workerEndpoints: EndpointPair = {
|
||||
proxy: clientAPIChannel.port2,
|
||||
expose: extensionHostAPIChannel.port2,
|
||||
}
|
||||
worker.postMessage({ endpoints: workerEndpoints, wrapEndpoints }, Object.values(workerEndpoints))
|
||||
const clientEndpoints = {
|
||||
proxy: extensionHostAPIChannel.port1,
|
||||
expose: clientAPIChannel.port1,
|
||||
}
|
||||
return { worker, clientEndpoints }
|
||||
}
|
||||
|
||||
export function createExtensionHost({ wrapEndpoints }: ExtensionHostInitOptions): Observable<EndpointPair> {
|
||||
return new Observable(subscriber => {
|
||||
const { clientEndpoints, worker } = createExtensionHostWorker({ wrapEndpoints })
|
||||
subscriber.next(clientEndpoints)
|
||||
return () => worker.terminate()
|
||||
})
|
||||
}
|
||||
@ -6,18 +6,18 @@ import { integrationTestContext } from './testHelpers'
|
||||
describe('CodeEditor (integration)', () => {
|
||||
describe('setDecorations', () => {
|
||||
test('adds decorations', async () => {
|
||||
const { services, extensionHost } = await integrationTestContext()
|
||||
const dt = extensionHost.app.createDecorationType()
|
||||
const { services, extensionAPI } = await integrationTestContext()
|
||||
const dt = extensionAPI.app.createDecorationType()
|
||||
|
||||
// Set some decorations and check they are present on the client.
|
||||
const codeEditor = extensionHost.app.windows[0].visibleViewComponents[0]
|
||||
const codeEditor = extensionAPI.app.windows[0].visibleViewComponents[0]
|
||||
codeEditor.setDecorations(dt, [
|
||||
{
|
||||
range: new Range(1, 2, 3, 4),
|
||||
backgroundColor: 'red',
|
||||
},
|
||||
])
|
||||
await extensionHost.internal.sync()
|
||||
await extensionAPI.internal.sync()
|
||||
expect(
|
||||
await services.textDocumentDecoration
|
||||
.getDecorations({ uri: 'file:///f' })
|
||||
@ -32,7 +32,7 @@ describe('CodeEditor (integration)', () => {
|
||||
|
||||
// Clear the decorations and ensure they are removed.
|
||||
codeEditor.setDecorations(dt, [])
|
||||
await extensionHost.internal.sync()
|
||||
await extensionAPI.internal.sync()
|
||||
expect(
|
||||
await services.textDocumentDecoration
|
||||
.getDecorations({ uri: 'file:///f' })
|
||||
@ -42,10 +42,10 @@ describe('CodeEditor (integration)', () => {
|
||||
})
|
||||
|
||||
it('merges decorations from several types', async () => {
|
||||
const { services, extensionHost } = await integrationTestContext()
|
||||
const [dt1, dt2] = [extensionHost.app.createDecorationType(), extensionHost.app.createDecorationType()]
|
||||
const { services, extensionAPI } = await integrationTestContext()
|
||||
const [dt1, dt2] = [extensionAPI.app.createDecorationType(), extensionAPI.app.createDecorationType()]
|
||||
|
||||
const codeEditor = extensionHost.app.windows[0].visibleViewComponents[0]
|
||||
const codeEditor = extensionAPI.app.windows[0].visibleViewComponents[0]
|
||||
codeEditor.setDecorations(dt1, [
|
||||
{
|
||||
range: new Range(1, 2, 3, 4),
|
||||
@ -62,7 +62,7 @@ describe('CodeEditor (integration)', () => {
|
||||
},
|
||||
},
|
||||
])
|
||||
await extensionHost.internal.sync()
|
||||
await extensionAPI.internal.sync()
|
||||
expect(
|
||||
await services.textDocumentDecoration
|
||||
.getDecorations({ uri: 'file:///f' })
|
||||
@ -92,7 +92,7 @@ describe('CodeEditor (integration)', () => {
|
||||
},
|
||||
},
|
||||
])
|
||||
await extensionHost.internal.sync()
|
||||
await extensionAPI.internal.sync()
|
||||
expect(
|
||||
await services.textDocumentDecoration
|
||||
.getDecorations({ uri: 'file:///f' })
|
||||
@ -115,7 +115,7 @@ describe('CodeEditor (integration)', () => {
|
||||
|
||||
// remove decorations for dt2, and verify that decorations for dt1 are still present
|
||||
codeEditor.setDecorations(dt2, [])
|
||||
await extensionHost.internal.sync()
|
||||
await extensionAPI.internal.sync()
|
||||
expect(
|
||||
await services.textDocumentDecoration
|
||||
.getDecorations({ uri: 'file:///f' })
|
||||
@ -132,11 +132,11 @@ describe('CodeEditor (integration)', () => {
|
||||
})
|
||||
|
||||
it('is backwards compatible with extensions that do not provide a decoration type', async () => {
|
||||
const { services, extensionHost } = await integrationTestContext()
|
||||
const dt = extensionHost.app.createDecorationType()
|
||||
const { services, extensionAPI } = await integrationTestContext()
|
||||
const dt = extensionAPI.app.createDecorationType()
|
||||
|
||||
// Set some decorations and check they are present on the client.
|
||||
const codeEditor = extensionHost.app.windows[0].visibleViewComponents[0]
|
||||
const codeEditor = extensionAPI.app.windows[0].visibleViewComponents[0]
|
||||
codeEditor.setDecorations(dt, [
|
||||
{
|
||||
range: new Range(1, 2, 3, 4),
|
||||
@ -156,7 +156,7 @@ describe('CodeEditor (integration)', () => {
|
||||
])
|
||||
|
||||
// Both sets of decorations should be displayed
|
||||
await extensionHost.internal.sync()
|
||||
await extensionAPI.internal.sync()
|
||||
expect(
|
||||
await services.textDocumentDecoration
|
||||
.getDecorations({ uri: 'file:///f' })
|
||||
|
||||
@ -3,33 +3,33 @@ import { integrationTestContext } from './testHelpers'
|
||||
describe('Commands (integration)', () => {
|
||||
describe('commands.registerCommand', () => {
|
||||
test('registers and unregisters a single command', async () => {
|
||||
const { services, extensionHost } = await integrationTestContext()
|
||||
const { services, extensionAPI } = await integrationTestContext()
|
||||
|
||||
// Register the command and call it.
|
||||
const unsubscribe = extensionHost.commands.registerCommand('c', () => 'a')
|
||||
await expect(extensionHost.commands.executeCommand('c')).resolves.toBe('a')
|
||||
const unsubscribe = extensionAPI.commands.registerCommand('c', () => 'a')
|
||||
await expect(extensionAPI.commands.executeCommand('c')).resolves.toBe('a')
|
||||
await expect(services.commands.executeCommand({ command: 'c' })).resolves.toBe('a')
|
||||
|
||||
// Unregister the command and ensure it's removed.
|
||||
unsubscribe.unsubscribe()
|
||||
await extensionHost.internal.sync()
|
||||
await expect(extensionHost.commands.executeCommand('c')).rejects.toMatchObject({
|
||||
await extensionAPI.internal.sync()
|
||||
await expect(extensionAPI.commands.executeCommand('c')).rejects.toMatchObject({
|
||||
message: 'command not found: "c"',
|
||||
})
|
||||
expect(() => services.commands.executeCommand({ command: 'c' })).toThrow()
|
||||
})
|
||||
|
||||
test('supports multiple commands', async () => {
|
||||
const { services, extensionHost } = await integrationTestContext()
|
||||
const { services, extensionAPI } = await integrationTestContext()
|
||||
|
||||
// Register 2 commands with different results.
|
||||
extensionHost.commands.registerCommand('c1', () => 'a1')
|
||||
extensionHost.commands.registerCommand('c2', () => 'a2')
|
||||
await extensionHost.internal.sync()
|
||||
extensionAPI.commands.registerCommand('c1', () => 'a1')
|
||||
extensionAPI.commands.registerCommand('c2', () => 'a2')
|
||||
await extensionAPI.internal.sync()
|
||||
|
||||
await expect(extensionHost.commands.executeCommand('c1')).resolves.toBe('a1')
|
||||
await expect(extensionAPI.commands.executeCommand('c1')).resolves.toBe('a1')
|
||||
await expect(services.commands.executeCommand({ command: 'c1' })).resolves.toBe('a1')
|
||||
await expect(extensionHost.commands.executeCommand('c2')).resolves.toBe('a2')
|
||||
await expect(extensionAPI.commands.executeCommand('c2')).resolves.toBe('a2')
|
||||
await expect(services.commands.executeCommand({ command: 'c2' })).resolves.toBe('a2')
|
||||
})
|
||||
})
|
||||
|
||||
@ -6,36 +6,38 @@ import { integrationTestContext } from './testHelpers'
|
||||
|
||||
describe('Configuration (integration)', () => {
|
||||
test('is synchronously available', async () => {
|
||||
const { extensionHost } = await integrationTestContext({ settings: of(EMPTY_SETTINGS_CASCADE) })
|
||||
expect(() => extensionHost.configuration.subscribe(() => void 0)).not.toThrow()
|
||||
expect(() => extensionHost.configuration.get()).not.toThrow()
|
||||
const { extensionAPI } = await integrationTestContext({ settings: of(EMPTY_SETTINGS_CASCADE) })
|
||||
expect(() => extensionAPI.configuration.subscribe(() => void 0)).not.toThrow()
|
||||
expect(() => extensionAPI.configuration.get()).not.toThrow()
|
||||
})
|
||||
|
||||
describe('Configuration#get', () => {
|
||||
test('gets configuration', async () => {
|
||||
const { extensionHost } = await integrationTestContext({ settings: of({ final: { a: 1 }, subjects: [] }) })
|
||||
assertToJSON(extensionHost.configuration.get(), { a: 1 })
|
||||
expect(extensionHost.configuration.get().value).toEqual({ a: 1 })
|
||||
const { extensionAPI } = await integrationTestContext({
|
||||
settings: of({ final: { a: 1 }, subjects: [] }),
|
||||
})
|
||||
assertToJSON(extensionAPI.configuration.get(), { a: 1 })
|
||||
expect(extensionAPI.configuration.get().value).toEqual({ a: 1 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Configuration#update', () => {
|
||||
test('updates configuration', async () => {
|
||||
const calls: (SettingsEdit | string)[] = []
|
||||
const { extensionHost } = await integrationTestContext({
|
||||
const { extensionAPI } = await integrationTestContext({
|
||||
settings: of({ final: { a: 1 }, subjects: [{ subject: {} as any, lastID: null, settings: null }] }),
|
||||
updateSettings: async (_subject, edit) => {
|
||||
calls.push(edit)
|
||||
},
|
||||
})
|
||||
|
||||
await extensionHost.configuration.get().update('a', 2)
|
||||
await extensionHost.internal.sync()
|
||||
await extensionAPI.configuration.get().update('a', 2)
|
||||
await extensionAPI.internal.sync()
|
||||
expect(calls).toEqual([{ path: ['a'], value: 2 }] as SettingsEdit[])
|
||||
calls.length = 0 // clear
|
||||
|
||||
await extensionHost.configuration.get().update('a', 3)
|
||||
await extensionHost.internal.sync()
|
||||
await extensionAPI.configuration.get().update('a', 3)
|
||||
await extensionAPI.internal.sync()
|
||||
expect(calls).toEqual([{ path: ['a'], value: 3 }] as SettingsEdit[])
|
||||
})
|
||||
})
|
||||
@ -43,14 +45,14 @@ describe('Configuration (integration)', () => {
|
||||
describe('configuration.subscribe', () => {
|
||||
test('subscribes to changes', async () => {
|
||||
const mockSettings = new BehaviorSubject<SettingsCascadeOrError>(EMPTY_SETTINGS_CASCADE)
|
||||
const { extensionHost } = await integrationTestContext({ settings: mockSettings })
|
||||
const { extensionAPI } = await integrationTestContext({ settings: mockSettings })
|
||||
|
||||
let calls = 0
|
||||
extensionHost.configuration.subscribe(() => calls++)
|
||||
extensionAPI.configuration.subscribe(() => calls++)
|
||||
expect(calls).toBe(1) // called initially
|
||||
|
||||
mockSettings.next(EMPTY_SETTINGS_CASCADE)
|
||||
await extensionHost.internal.sync()
|
||||
await extensionAPI.internal.sync()
|
||||
expect(calls).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
@ -6,11 +6,11 @@ import { collectSubscribableValues, integrationTestContext } from './testHelpers
|
||||
describe('Context (integration)', () => {
|
||||
describe('internal.updateContext', () => {
|
||||
test('updates context', async () => {
|
||||
const { services, extensionHost } = await integrationTestContext()
|
||||
const { services, extensionAPI } = await integrationTestContext()
|
||||
const values = collectSubscribableValues(from(services.context.data).pipe(distinctUntilChanged()))
|
||||
|
||||
extensionHost.internal.updateContext({ a: 1 })
|
||||
await extensionHost.internal.sync()
|
||||
extensionAPI.internal.updateContext({ a: 1 })
|
||||
await extensionAPI.internal.sync()
|
||||
expect(values).toEqual([
|
||||
{ 'clientApplication.isSourcegraph': true, 'clientApplication.extensionAPIVersion.major': 3 },
|
||||
{ a: 1, 'clientApplication.isSourcegraph': true, 'clientApplication.extensionAPIVersion.major': 3 },
|
||||
|
||||
@ -4,14 +4,14 @@ import { collectSubscribableValues, integrationTestContext } from './testHelpers
|
||||
describe('Documents (integration)', () => {
|
||||
describe('workspace.textDocuments', () => {
|
||||
test('lists text documents', async () => {
|
||||
const { extensionHost } = await integrationTestContext()
|
||||
expect(extensionHost.workspace.textDocuments).toEqual([
|
||||
const { extensionAPI } = await integrationTestContext()
|
||||
expect(extensionAPI.workspace.textDocuments).toEqual([
|
||||
{ uri: 'file:///f', languageId: 'l', text: 't' },
|
||||
] as TextDocument[])
|
||||
})
|
||||
|
||||
test('adds new text documents', async () => {
|
||||
const { model, extensionHost } = await integrationTestContext()
|
||||
const { model, extensionAPI } = await integrationTestContext()
|
||||
model.next({
|
||||
...model.value,
|
||||
visibleViewComponents: [
|
||||
@ -23,8 +23,8 @@ describe('Documents (integration)', () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
await extensionHost.internal.sync()
|
||||
expect(extensionHost.workspace.textDocuments).toEqual([
|
||||
await extensionAPI.internal.sync()
|
||||
expect(extensionAPI.workspace.textDocuments).toEqual([
|
||||
{ uri: 'file:///f', languageId: 'l', text: 't' },
|
||||
{ uri: 'file:///f2', languageId: 'l2', text: 't2' },
|
||||
] as TextDocument[])
|
||||
@ -33,9 +33,9 @@ describe('Documents (integration)', () => {
|
||||
|
||||
describe('workspace.openedTextDocuments', () => {
|
||||
test('fires when a text document is opened', async () => {
|
||||
const { model, extensionHost } = await integrationTestContext()
|
||||
const { model, extensionAPI } = await integrationTestContext()
|
||||
|
||||
const values = collectSubscribableValues(extensionHost.workspace.openedTextDocuments)
|
||||
const values = collectSubscribableValues(extensionAPI.workspace.openedTextDocuments)
|
||||
expect(values).toEqual([] as TextDocument[])
|
||||
|
||||
model.next({
|
||||
@ -49,7 +49,7 @@ describe('Documents (integration)', () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
await extensionHost.internal.sync()
|
||||
await extensionAPI.internal.sync()
|
||||
|
||||
expect(values).toEqual([{ uri: 'file:///f2', languageId: 'l2', text: 't2' }] as TextDocument[])
|
||||
})
|
||||
|
||||
@ -2,8 +2,8 @@ import { integrationTestContext } from './testHelpers'
|
||||
|
||||
describe('Internal (integration)', () => {
|
||||
test('constant values', async () => {
|
||||
const { extensionHost } = await integrationTestContext()
|
||||
expect(extensionHost.internal.sourcegraphURL.toString()).toEqual('https://example.com')
|
||||
expect(extensionHost.internal.clientApplication).toEqual('sourcegraph')
|
||||
const { extensionAPI } = await integrationTestContext()
|
||||
expect(extensionAPI.internal.sourcegraphURL.toString()).toEqual('https://example.com')
|
||||
expect(extensionAPI.internal.clientApplication).toEqual('sourcegraph')
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Location } from '@sourcegraph/extension-api-types'
|
||||
import { Observable } from 'rxjs'
|
||||
import { bufferCount, take } from 'rxjs/operators'
|
||||
import { asyncScheduler, Observable, of } from 'rxjs'
|
||||
import { observeOn, take, toArray } from 'rxjs/operators'
|
||||
import * as sourcegraph from 'sourcegraph'
|
||||
import { languages as sourcegraphLanguages } from 'sourcegraph'
|
||||
import { Services } from '../client/services'
|
||||
@ -9,86 +9,84 @@ import { URI } from '../extension/types/uri'
|
||||
import { createBarrier, integrationTestContext } from './testHelpers'
|
||||
|
||||
describe('LanguageFeatures (integration)', () => {
|
||||
testLocationProvider<sourcegraph.HoverProvider>(
|
||||
'registerHoverProvider',
|
||||
extensionHost => extensionHost.languages.registerHoverProvider,
|
||||
label => ({
|
||||
provideHover: (doc: sourcegraph.TextDocument, pos: sourcegraph.Position) => ({
|
||||
contents: { value: label, kind: sourcegraph.MarkupKind.PlainText },
|
||||
}),
|
||||
testLocationProvider<sourcegraph.HoverProvider>({
|
||||
name: 'registerHoverProvider',
|
||||
registerProvider: extensionAPI => (s, p) => extensionAPI.languages.registerHoverProvider(s, p),
|
||||
labeledProvider: label => ({
|
||||
provideHover: (doc: sourcegraph.TextDocument, pos: sourcegraph.Position) =>
|
||||
of({
|
||||
contents: { value: label, kind: sourcegraph.MarkupKind.PlainText },
|
||||
}).pipe(observeOn(asyncScheduler)),
|
||||
}),
|
||||
labels => ({
|
||||
labeledProviderResults: labels => ({
|
||||
contents: labels.map(label => ({ value: label, kind: sourcegraph.MarkupKind.PlainText })),
|
||||
}),
|
||||
run => ({ provideHover: run } as sourcegraph.HoverProvider),
|
||||
services =>
|
||||
providerWithImplementation: run => ({ provideHover: run } as sourcegraph.HoverProvider),
|
||||
getResult: services =>
|
||||
services.textDocumentHover.getHover({
|
||||
textDocument: { uri: 'file:///f' },
|
||||
position: { line: 1, character: 2 },
|
||||
})
|
||||
)
|
||||
testLocationProvider<sourcegraph.DefinitionProvider>(
|
||||
'registerDefinitionProvider',
|
||||
extensionHost => extensionHost.languages.registerDefinitionProvider,
|
||||
label => ({
|
||||
provideDefinition: (doc: sourcegraph.TextDocument, pos: sourcegraph.Position) => [
|
||||
{ uri: new URI(`file:///${label}`) },
|
||||
],
|
||||
}),
|
||||
})
|
||||
testLocationProvider<sourcegraph.DefinitionProvider>({
|
||||
name: 'registerDefinitionProvider',
|
||||
registerProvider: extensionAPI => extensionAPI.languages.registerDefinitionProvider,
|
||||
labeledProvider: label => ({
|
||||
provideDefinition: (doc: sourcegraph.TextDocument, pos: sourcegraph.Position) =>
|
||||
of([{ uri: new URI(`file:///${label}`) }]).pipe(observeOn(asyncScheduler)),
|
||||
}),
|
||||
labeledDefinitionResults,
|
||||
run => ({ provideDefinition: run } as sourcegraph.DefinitionProvider),
|
||||
services =>
|
||||
labeledProviderResults: labeledDefinitionResults,
|
||||
providerWithImplementation: run => ({ provideDefinition: run } as sourcegraph.DefinitionProvider),
|
||||
getResult: services =>
|
||||
services.textDocumentDefinition.getLocations({
|
||||
textDocument: { uri: 'file:///f' },
|
||||
position: { line: 1, character: 2 },
|
||||
})
|
||||
)
|
||||
}),
|
||||
})
|
||||
// tslint:disable deprecation The tests must remain until they are removed.
|
||||
testLocationProvider(
|
||||
'registerTypeDefinitionProvider',
|
||||
extensionHost => extensionHost.languages.registerTypeDefinitionProvider,
|
||||
label => ({
|
||||
provideTypeDefinition: (doc: sourcegraph.TextDocument, pos: sourcegraph.Position) => [
|
||||
{ uri: new URI(`file:///${label}`) },
|
||||
],
|
||||
testLocationProvider({
|
||||
name: 'registerTypeDefinitionProvider',
|
||||
registerProvider: extensionAPI => extensionAPI.languages.registerTypeDefinitionProvider,
|
||||
labeledProvider: label => ({
|
||||
provideTypeDefinition: (doc: sourcegraph.TextDocument, pos: sourcegraph.Position) =>
|
||||
of([{ uri: new URI(`file:///${label}`) }]).pipe(observeOn(asyncScheduler)),
|
||||
}),
|
||||
labeledDefinitionResults,
|
||||
run => ({ provideTypeDefinition: run } as sourcegraph.TypeDefinitionProvider),
|
||||
services =>
|
||||
labeledProviderResults: labeledDefinitionResults,
|
||||
providerWithImplementation: run => ({ provideTypeDefinition: run } as sourcegraph.TypeDefinitionProvider),
|
||||
getResult: services =>
|
||||
services.textDocumentTypeDefinition.getLocations({
|
||||
textDocument: { uri: 'file:///f' },
|
||||
position: { line: 1, character: 2 },
|
||||
})
|
||||
)
|
||||
testLocationProvider<sourcegraph.ImplementationProvider>(
|
||||
'registerImplementationProvider',
|
||||
extensionHost => extensionHost.languages.registerImplementationProvider,
|
||||
label => ({
|
||||
provideImplementation: (doc: sourcegraph.TextDocument, pos: sourcegraph.Position) => [
|
||||
{ uri: new URI(`file:///${label}`) },
|
||||
],
|
||||
}),
|
||||
})
|
||||
testLocationProvider<sourcegraph.ImplementationProvider>({
|
||||
name: 'registerImplementationProvider',
|
||||
registerProvider: extensionAPI => extensionAPI.languages.registerImplementationProvider,
|
||||
labeledProvider: label => ({
|
||||
provideImplementation: (doc: sourcegraph.TextDocument, pos: sourcegraph.Position) =>
|
||||
of([{ uri: new URI(`file:///${label}`) }]).pipe(observeOn(asyncScheduler)),
|
||||
}),
|
||||
labeledDefinitionResults,
|
||||
run => ({ provideImplementation: run } as sourcegraph.ImplementationProvider),
|
||||
services =>
|
||||
labeledProviderResults: labeledDefinitionResults,
|
||||
providerWithImplementation: run => ({ provideImplementation: run } as sourcegraph.ImplementationProvider),
|
||||
getResult: services =>
|
||||
services.textDocumentImplementation.getLocations({
|
||||
textDocument: { uri: 'file:///f' },
|
||||
position: { line: 1, character: 2 },
|
||||
})
|
||||
)
|
||||
}),
|
||||
})
|
||||
// tslint:enable deprecation
|
||||
testLocationProvider<sourcegraph.ReferenceProvider>(
|
||||
'registerReferenceProvider',
|
||||
extensionHost => extensionHost.languages.registerReferenceProvider,
|
||||
label => ({
|
||||
testLocationProvider<sourcegraph.ReferenceProvider>({
|
||||
name: 'registerReferenceProvider',
|
||||
registerProvider: extensionAPI => extensionAPI.languages.registerReferenceProvider,
|
||||
labeledProvider: label => ({
|
||||
provideReferences: (
|
||||
doc: sourcegraph.TextDocument,
|
||||
pos: sourcegraph.Position,
|
||||
context: sourcegraph.ReferenceContext
|
||||
) => [{ uri: new URI(`file:///${label}`) }],
|
||||
) => of([{ uri: new URI(`file:///${label}`) }]).pipe(observeOn(asyncScheduler)),
|
||||
}),
|
||||
labels => labels.map(label => ({ uri: `file:///${label}`, range: undefined })),
|
||||
run =>
|
||||
labeledProviderResults: labels => labels.map(label => ({ uri: `file:///${label}`, range: undefined })),
|
||||
providerWithImplementation: run =>
|
||||
({
|
||||
provideReferences: (
|
||||
doc: sourcegraph.TextDocument,
|
||||
@ -96,56 +94,62 @@ describe('LanguageFeatures (integration)', () => {
|
||||
_context: sourcegraph.ReferenceContext
|
||||
) => run(doc, pos),
|
||||
} as sourcegraph.ReferenceProvider),
|
||||
services =>
|
||||
getResult: services =>
|
||||
services.textDocumentReferences.getLocations({
|
||||
textDocument: { uri: 'file:///f' },
|
||||
position: { line: 1, character: 2 },
|
||||
context: { includeDeclaration: true },
|
||||
})
|
||||
)
|
||||
testLocationProvider<sourcegraph.LocationProvider>(
|
||||
'registerLocationProvider',
|
||||
extensionHost => (selector, provider) =>
|
||||
extensionHost.languages.registerLocationProvider('x', selector, provider),
|
||||
label => ({
|
||||
provideLocations: (doc: sourcegraph.TextDocument, pos: sourcegraph.Position) => [
|
||||
{ uri: new URI(`file:///${label}`) },
|
||||
],
|
||||
}),
|
||||
})
|
||||
testLocationProvider<sourcegraph.LocationProvider>({
|
||||
name: 'registerLocationProvider',
|
||||
registerProvider: extensionAPI => (selector, provider) =>
|
||||
extensionAPI.languages.registerLocationProvider('x', selector, provider),
|
||||
labeledProvider: label => ({
|
||||
provideLocations: (doc: sourcegraph.TextDocument, pos: sourcegraph.Position) =>
|
||||
of([{ uri: new URI(`file:///${label}`) }]).pipe(observeOn(asyncScheduler)),
|
||||
}),
|
||||
labels => labels.map(label => ({ uri: `file:///${label}`, range: undefined })),
|
||||
run =>
|
||||
labeledProviderResults: labels => labels.map(label => ({ uri: `file:///${label}`, range: undefined })),
|
||||
providerWithImplementation: run =>
|
||||
({
|
||||
provideLocations: (doc: sourcegraph.TextDocument, pos: sourcegraph.Position) => run(doc, pos),
|
||||
} as sourcegraph.LocationProvider),
|
||||
services =>
|
||||
getResult: services =>
|
||||
services.textDocumentLocations.getLocations('x', {
|
||||
textDocument: { uri: 'file:///f' },
|
||||
position: { line: 1, character: 2 },
|
||||
})
|
||||
)
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Generates test cases for sourcegraph.languages.registerXyzProvider functions and their associated
|
||||
* XyzProviders, for providers that return a list of locations.
|
||||
*/
|
||||
function testLocationProvider<P>(
|
||||
name: keyof typeof sourcegraphLanguages,
|
||||
function testLocationProvider<P>({
|
||||
name,
|
||||
registerProvider,
|
||||
labeledProvider,
|
||||
labeledProviderResults,
|
||||
providerWithImplementation,
|
||||
getResult,
|
||||
}: {
|
||||
name: keyof typeof sourcegraphLanguages
|
||||
registerProvider: (
|
||||
extensionHost: typeof sourcegraph
|
||||
) => (selector: sourcegraph.DocumentSelector, provider: P) => sourcegraph.Unsubscribable,
|
||||
labeledProvider: (label: string) => P,
|
||||
labeledProviderResults: (labels: string[]) => any,
|
||||
providerWithImpl: (run: (doc: sourcegraph.TextDocument, pos: sourcegraph.Position) => void) => P,
|
||||
extensionAPI: typeof sourcegraph
|
||||
) => (selector: sourcegraph.DocumentSelector, provider: P) => sourcegraph.Unsubscribable
|
||||
labeledProvider: (label: string) => P
|
||||
labeledProviderResults: (labels: string[]) => any
|
||||
providerWithImplementation: (run: (doc: sourcegraph.TextDocument, pos: sourcegraph.Position) => void) => P
|
||||
getResult: (services: Services) => Observable<any>
|
||||
): void {
|
||||
}): void {
|
||||
describe(`languages.${name}`, () => {
|
||||
test('registers and unregisters a single provider', async () => {
|
||||
const { services, extensionHost } = await integrationTestContext()
|
||||
const { services, extensionAPI } = await integrationTestContext()
|
||||
|
||||
// Register the provider and call it.
|
||||
const unsubscribe = registerProvider(extensionHost)(['*'], labeledProvider('a'))
|
||||
await extensionHost.internal.sync()
|
||||
const subscription = registerProvider(extensionAPI)(['*'], labeledProvider('a'))
|
||||
await extensionAPI.internal.sync()
|
||||
expect(
|
||||
await getResult(services)
|
||||
.pipe(take(1))
|
||||
@ -153,7 +157,7 @@ function testLocationProvider<P>(
|
||||
).toEqual(labeledProviderResults(['a']))
|
||||
|
||||
// Unregister the provider and ensure it's removed.
|
||||
unsubscribe.unsubscribe()
|
||||
subscription.unsubscribe()
|
||||
expect(
|
||||
await getResult(services)
|
||||
.pipe(take(1))
|
||||
@ -162,17 +166,17 @@ function testLocationProvider<P>(
|
||||
})
|
||||
|
||||
test('supplies params to the provideXyz method', async () => {
|
||||
const { services, extensionHost } = await integrationTestContext()
|
||||
const { services, extensionAPI } = await integrationTestContext()
|
||||
const { wait, done } = createBarrier()
|
||||
registerProvider(extensionHost)(
|
||||
registerProvider(extensionAPI)(
|
||||
['*'],
|
||||
providerWithImpl((doc, pos) => {
|
||||
providerWithImplementation((doc, pos) => {
|
||||
assertToJSON(doc, { uri: 'file:///f', languageId: 'l', text: 't' })
|
||||
assertToJSON(pos, { line: 1, character: 2 })
|
||||
done()
|
||||
})
|
||||
)
|
||||
await extensionHost.internal.sync()
|
||||
await extensionAPI.internal.sync()
|
||||
await getResult(services)
|
||||
.pipe(take(1))
|
||||
.toPromise()
|
||||
@ -180,19 +184,19 @@ function testLocationProvider<P>(
|
||||
})
|
||||
|
||||
test('supports multiple providers', async () => {
|
||||
const { services, extensionHost } = await integrationTestContext()
|
||||
const { services, extensionAPI } = await integrationTestContext()
|
||||
|
||||
// Register 2 providers with different results.
|
||||
registerProvider(extensionHost)(['*'], labeledProvider('a'))
|
||||
registerProvider(extensionHost)(['*'], labeledProvider('b'))
|
||||
await extensionHost.internal.sync()
|
||||
registerProvider(extensionAPI)(['*'], labeledProvider('a'))
|
||||
registerProvider(extensionAPI)(['*'], labeledProvider('b'))
|
||||
await extensionAPI.internal.sync()
|
||||
|
||||
// Expect it to emit the first provider's result first (and not block on both providers being ready).
|
||||
expect(
|
||||
await getResult(services)
|
||||
.pipe(
|
||||
take(2),
|
||||
bufferCount(2)
|
||||
toArray()
|
||||
)
|
||||
.toPromise()
|
||||
).toEqual([labeledProviderResults(['a']), labeledProviderResults(['a', 'b'])])
|
||||
|
||||
@ -5,20 +5,20 @@ import { collectSubscribableValues, integrationTestContext } from './testHelpers
|
||||
describe('Workspace roots (integration)', () => {
|
||||
describe('workspace.roots', () => {
|
||||
test('lists roots', async () => {
|
||||
const { extensionHost } = await integrationTestContext()
|
||||
expect(extensionHost.workspace.roots).toEqual([{ uri: new URI('file:///') }] as WorkspaceRoot[])
|
||||
const { extensionAPI } = await integrationTestContext()
|
||||
expect(extensionAPI.workspace.roots).toEqual([{ uri: new URI('file:///') }] as WorkspaceRoot[])
|
||||
})
|
||||
|
||||
test('adds new text documents', async () => {
|
||||
const { model, extensionHost } = await integrationTestContext()
|
||||
const { model, extensionAPI } = await integrationTestContext()
|
||||
|
||||
model.next({
|
||||
...model.value,
|
||||
roots: [{ uri: 'file:///a' }, { uri: 'file:///b' }],
|
||||
})
|
||||
await extensionHost.internal.sync()
|
||||
await extensionAPI.internal.sync()
|
||||
|
||||
expect(extensionHost.workspace.roots).toEqual([
|
||||
expect(extensionAPI.workspace.roots).toEqual([
|
||||
{ uri: new URI('file:///a') },
|
||||
{ uri: new URI('file:///b') },
|
||||
] as WorkspaceRoot[])
|
||||
@ -27,16 +27,16 @@ describe('Workspace roots (integration)', () => {
|
||||
|
||||
describe('workspace.rootChanges', () => {
|
||||
test('fires when a root is added or removed', async () => {
|
||||
const { model, extensionHost } = await integrationTestContext()
|
||||
const { model, extensionAPI } = await integrationTestContext()
|
||||
|
||||
const values = collectSubscribableValues(extensionHost.workspace.rootChanges)
|
||||
const values = collectSubscribableValues(extensionAPI.workspace.rootChanges)
|
||||
expect(values).toEqual([] as void[])
|
||||
|
||||
model.next({
|
||||
...model.value,
|
||||
roots: [{ uri: 'file:///a' }],
|
||||
})
|
||||
await extensionHost.internal.sync()
|
||||
await extensionAPI.internal.sync()
|
||||
|
||||
expect(values).toEqual([void 0])
|
||||
})
|
||||
|
||||
@ -3,20 +3,32 @@ import { integrationTestContext } from './testHelpers'
|
||||
|
||||
describe('search (integration)', () => {
|
||||
test('registers a query transformer', async () => {
|
||||
const { services, extensionHost } = await integrationTestContext()
|
||||
const { services, extensionAPI } = await integrationTestContext()
|
||||
|
||||
// Register the provider and call it
|
||||
const unsubscribe = extensionHost.search.registerQueryTransformer({ transformQuery: () => 'bar' })
|
||||
await extensionHost.internal.sync()
|
||||
extensionAPI.search.registerQueryTransformer({
|
||||
transformQuery: () => 'bar',
|
||||
})
|
||||
await extensionAPI.internal.sync()
|
||||
expect(
|
||||
await services.queryTransformer
|
||||
.transformQuery('foo')
|
||||
.pipe(take(1))
|
||||
.toPromise()
|
||||
).toEqual('bar')
|
||||
})
|
||||
|
||||
test('unregisters a query transformer', async () => {
|
||||
const { services, extensionAPI } = await integrationTestContext()
|
||||
|
||||
// Register the provider and call it
|
||||
const subscription = extensionAPI.search.registerQueryTransformer({
|
||||
transformQuery: () => 'bar',
|
||||
})
|
||||
await extensionAPI.internal.sync()
|
||||
// Unregister the provider and ensure it's removed.
|
||||
unsubscribe.unsubscribe()
|
||||
subscription.unsubscribe()
|
||||
await extensionAPI.internal.sync()
|
||||
expect(
|
||||
await services.queryTransformer
|
||||
.transformQuery('foo')
|
||||
@ -26,12 +38,12 @@ describe('search (integration)', () => {
|
||||
})
|
||||
|
||||
test('supports multiple query transformers', async () => {
|
||||
const { services, extensionHost } = await integrationTestContext()
|
||||
const { services, extensionAPI } = await integrationTestContext()
|
||||
|
||||
// Register the provider and call it
|
||||
extensionHost.search.registerQueryTransformer({ transformQuery: (q: string) => `${q} bar` })
|
||||
extensionHost.search.registerQueryTransformer({ transformQuery: (q: string) => `${q} qux` })
|
||||
await extensionHost.internal.sync()
|
||||
extensionAPI.search.registerQueryTransformer({ transformQuery: (q: string) => `${q} bar` })
|
||||
extensionAPI.search.registerQueryTransformer({ transformQuery: (q: string) => `${q} qux` })
|
||||
await extensionAPI.internal.sync()
|
||||
expect(
|
||||
await services.queryTransformer
|
||||
.transformQuery('foo')
|
||||
|
||||
@ -33,11 +33,11 @@ const withSelections = (...selections: { start: number; end: number }[]): ViewCo
|
||||
describe('Selections (integration)', () => {
|
||||
describe('editor.selectionsChanged', () => {
|
||||
test('reflects changes to the current selections', async () => {
|
||||
const { model, extensionHost } = await integrationTestContext(undefined, {
|
||||
const { model, extensionAPI } = await integrationTestContext(undefined, {
|
||||
roots: [],
|
||||
visibleViewComponents: [],
|
||||
})
|
||||
const selectionChanges = from(extensionHost.app.activeWindowChanges).pipe(
|
||||
const selectionChanges = from(extensionAPI.app.activeWindowChanges).pipe(
|
||||
filter(isDefined),
|
||||
switchMap(window => window.activeViewComponentChanges),
|
||||
filter(isDefined),
|
||||
@ -54,7 +54,7 @@ describe('Selections (integration)', () => {
|
||||
...model.value,
|
||||
visibleViewComponents: [withSelections(...selections)],
|
||||
})
|
||||
await extensionHost.internal.sync()
|
||||
await extensionAPI.internal.sync()
|
||||
}
|
||||
assertToJSON(
|
||||
selectionValues.map(selections => selections.map(s => ({ start: s.start.line, end: s.end.line }))),
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { BehaviorSubject, NEVER, NextObserver, of, Subscribable, throwError } from 'rxjs'
|
||||
import { switchMap } from 'rxjs/operators'
|
||||
import 'message-port-polyfill'
|
||||
|
||||
import { BehaviorSubject, NEVER, NextObserver, Subscribable, throwError } from 'rxjs'
|
||||
import * as sourcegraph from 'sourcegraph'
|
||||
import { PlatformContext } from '../../platform/context'
|
||||
import { createExtensionHostClient, ExtensionHostClient } from '../client/client'
|
||||
import { EndpointPair, PlatformContext } from '../../platform/context'
|
||||
import { ExtensionHostClient } from '../client/client'
|
||||
import { createExtensionHostClientConnection } from '../client/connection'
|
||||
import { Model } from '../client/model'
|
||||
import { Services } from '../client/services'
|
||||
import { InitData, startExtensionHost } from '../extension/extensionHost'
|
||||
import { createConnection } from '../protocol/jsonrpc2/connection'
|
||||
import { createMessageTransports } from '../protocol/jsonrpc2/testHelpers'
|
||||
|
||||
const FIXTURE_MODEL: Model = {
|
||||
roots: [{ uri: 'file:///' }],
|
||||
@ -27,7 +27,7 @@ const FIXTURE_MODEL: Model = {
|
||||
|
||||
interface TestContext {
|
||||
client: ExtensionHostClient
|
||||
extensionHost: typeof sourcegraph
|
||||
extensionAPI: typeof sourcegraph
|
||||
}
|
||||
|
||||
interface Mocks
|
||||
@ -66,34 +66,32 @@ export async function integrationTestContext(
|
||||
> {
|
||||
const mocks = partialMocks ? { ...NOOP_MOCKS, ...partialMocks } : NOOP_MOCKS
|
||||
|
||||
const [clientTransports, serverTransports] = createMessageTransports()
|
||||
const clientAPIChannel = new MessageChannel()
|
||||
const extensionHostAPIChannel = new MessageChannel()
|
||||
const extensionHostEndpoints: EndpointPair = {
|
||||
proxy: clientAPIChannel.port2,
|
||||
expose: extensionHostAPIChannel.port2,
|
||||
}
|
||||
const clientEndpoints: EndpointPair = {
|
||||
proxy: extensionHostAPIChannel.port1,
|
||||
expose: clientAPIChannel.port1,
|
||||
}
|
||||
|
||||
const extensionHost = startExtensionHost(serverTransports)
|
||||
const extensionHost = startExtensionHost(extensionHostEndpoints)
|
||||
|
||||
const services = new Services(mocks)
|
||||
const client = createExtensionHostClient(
|
||||
services,
|
||||
of(clientTransports).pipe(
|
||||
switchMap(async clientTransports => {
|
||||
const connection = createConnection(clientTransports)
|
||||
connection.listen()
|
||||
|
||||
const initData: InitData = {
|
||||
sourcegraphURL: 'https://example.com',
|
||||
clientApplication: 'sourcegraph',
|
||||
}
|
||||
await connection.sendRequest('initialize', [initData])
|
||||
return connection
|
||||
})
|
||||
)
|
||||
)
|
||||
const initData: InitData = {
|
||||
sourcegraphURL: 'https://example.com',
|
||||
clientApplication: 'sourcegraph',
|
||||
}
|
||||
const client = await createExtensionHostClientConnection(clientEndpoints, services, initData)
|
||||
|
||||
services.model.model.next(initModel)
|
||||
|
||||
await (await extensionHost.__testAPI).internal.sync()
|
||||
const extensionAPI = await extensionHost.extensionAPI
|
||||
return {
|
||||
client,
|
||||
extensionHost: await extensionHost.__testAPI,
|
||||
extensionAPI,
|
||||
services,
|
||||
model: services.model.model,
|
||||
}
|
||||
|
||||
@ -7,12 +7,12 @@ import { collectSubscribableValues, integrationTestContext } from './testHelpers
|
||||
describe('Windows (integration)', () => {
|
||||
describe('app.activeWindow', () => {
|
||||
test('returns the active window', async () => {
|
||||
const { extensionHost } = await integrationTestContext()
|
||||
const { extensionAPI } = await integrationTestContext()
|
||||
const viewComponent: Pick<ViewComponent, 'type' | 'document'> = {
|
||||
type: 'CodeEditor' as 'CodeEditor',
|
||||
document: { uri: 'file:///f', languageId: 'l', text: 't' },
|
||||
}
|
||||
assertToJSON(extensionHost.app.activeWindow, {
|
||||
assertToJSON(extensionAPI.app.activeWindow, {
|
||||
visibleViewComponents: [viewComponent],
|
||||
activeViewComponent: viewComponent,
|
||||
} as Window)
|
||||
@ -21,12 +21,12 @@ describe('Windows (integration)', () => {
|
||||
|
||||
describe('app.activeWindowChanged', () => {
|
||||
test('reflects changes to the active window', async () => {
|
||||
const { extensionHost, model } = await integrationTestContext(undefined, {
|
||||
const { extensionAPI, model } = await integrationTestContext(undefined, {
|
||||
roots: [],
|
||||
visibleViewComponents: [],
|
||||
})
|
||||
await extensionHost.internal.sync()
|
||||
const values = collectSubscribableValues(extensionHost.app.activeWindowChanges)
|
||||
await extensionAPI.internal.sync()
|
||||
const values = collectSubscribableValues(extensionAPI.app.activeWindowChanges)
|
||||
model.next({
|
||||
...model.value,
|
||||
visibleViewComponents: [
|
||||
@ -42,7 +42,7 @@ describe('Windows (integration)', () => {
|
||||
...model.value,
|
||||
visibleViewComponents: [],
|
||||
})
|
||||
await extensionHost.internal.sync()
|
||||
await extensionAPI.internal.sync()
|
||||
model.next({
|
||||
...model.value,
|
||||
visibleViewComponents: [
|
||||
@ -54,7 +54,7 @@ describe('Windows (integration)', () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
await extensionHost.internal.sync()
|
||||
await extensionAPI.internal.sync()
|
||||
assertToJSON(values.map(w => w && w.activeViewComponent && w.activeViewComponent.document.uri), [
|
||||
null,
|
||||
'foo',
|
||||
@ -66,12 +66,12 @@ describe('Windows (integration)', () => {
|
||||
|
||||
describe('app.windows', () => {
|
||||
test('lists windows', async () => {
|
||||
const { extensionHost } = await integrationTestContext()
|
||||
const { extensionAPI } = await integrationTestContext()
|
||||
const viewComponent: Pick<ViewComponent, 'type' | 'document'> = {
|
||||
type: 'CodeEditor' as 'CodeEditor',
|
||||
document: { uri: 'file:///f', languageId: 'l', text: 't' },
|
||||
}
|
||||
assertToJSON(extensionHost.app.windows, [
|
||||
assertToJSON(extensionAPI.app.windows, [
|
||||
{
|
||||
visibleViewComponents: [viewComponent],
|
||||
activeViewComponent: viewComponent,
|
||||
@ -80,7 +80,7 @@ describe('Windows (integration)', () => {
|
||||
})
|
||||
|
||||
test('adds new text documents', async () => {
|
||||
const { model, extensionHost } = await integrationTestContext()
|
||||
const { model, extensionAPI } = await integrationTestContext()
|
||||
|
||||
model.next({
|
||||
...model.value,
|
||||
@ -93,13 +93,13 @@ describe('Windows (integration)', () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
await extensionHost.internal.sync()
|
||||
await extensionAPI.internal.sync()
|
||||
|
||||
const viewComponent: Pick<ViewComponent, 'type' | 'document'> = {
|
||||
type: 'CodeEditor' as 'CodeEditor',
|
||||
document: { uri: 'file:///f2', languageId: 'l2', text: 't2' },
|
||||
}
|
||||
assertToJSON(extensionHost.app.windows, [
|
||||
assertToJSON(extensionAPI.app.windows, [
|
||||
{
|
||||
visibleViewComponents: [viewComponent],
|
||||
activeViewComponent: viewComponent,
|
||||
@ -110,7 +110,7 @@ describe('Windows (integration)', () => {
|
||||
|
||||
describe('Window', () => {
|
||||
test('Window#visibleViewComponent', async () => {
|
||||
const { model, extensionHost } = await integrationTestContext()
|
||||
const { model, extensionAPI } = await integrationTestContext()
|
||||
|
||||
model.next({
|
||||
...model.value,
|
||||
@ -128,9 +128,9 @@ describe('Windows (integration)', () => {
|
||||
...(model.value.visibleViewComponents || []),
|
||||
],
|
||||
})
|
||||
await extensionHost.internal.sync()
|
||||
await extensionAPI.internal.sync()
|
||||
|
||||
assertToJSON(extensionHost.app.windows[0].visibleViewComponents, [
|
||||
assertToJSON(extensionAPI.app.windows[0].visibleViewComponents, [
|
||||
{
|
||||
type: 'CodeEditor' as 'CodeEditor',
|
||||
document: { uri: 'file:///inactive', languageId: 'inactive', text: 'inactive' },
|
||||
@ -143,7 +143,7 @@ describe('Windows (integration)', () => {
|
||||
})
|
||||
|
||||
test('Window#activeViewComponent', async () => {
|
||||
const { model, extensionHost } = await integrationTestContext()
|
||||
const { model, extensionAPI } = await integrationTestContext()
|
||||
|
||||
model.next({
|
||||
...model.value,
|
||||
@ -161,39 +161,39 @@ describe('Windows (integration)', () => {
|
||||
...(model.value.visibleViewComponents || []),
|
||||
],
|
||||
})
|
||||
await extensionHost.internal.sync()
|
||||
await extensionAPI.internal.sync()
|
||||
|
||||
assertToJSON(extensionHost.app.windows[0].activeViewComponent, {
|
||||
assertToJSON(extensionAPI.app.windows[0].activeViewComponent, {
|
||||
type: 'CodeEditor' as 'CodeEditor',
|
||||
document: { uri: 'file:///f', languageId: 'l', text: 't' },
|
||||
} as ViewComponent)
|
||||
})
|
||||
|
||||
test('Window#showNotification', async () => {
|
||||
const { extensionHost, services } = await integrationTestContext()
|
||||
const { extensionAPI, services } = await integrationTestContext()
|
||||
const values = collectSubscribableValues(services.notifications.showMessages)
|
||||
extensionHost.app.activeWindow!.showNotification('a') // tslint:disable-line deprecation
|
||||
await extensionHost.internal.sync()
|
||||
extensionAPI.app.activeWindow!.showNotification('a') // tslint:disable-line deprecation
|
||||
await extensionAPI.internal.sync()
|
||||
expect(values).toEqual([{ message: 'a', type: MessageType.Info }] as typeof values)
|
||||
})
|
||||
|
||||
test('Window#showMessage', async () => {
|
||||
const { extensionHost, services } = await integrationTestContext()
|
||||
const { extensionAPI, services } = await integrationTestContext()
|
||||
services.notifications.showMessageRequests.subscribe(({ resolve }) => resolve(Promise.resolve(null)))
|
||||
const values = collectSubscribableValues(
|
||||
services.notifications.showMessageRequests.pipe(map(({ message, type }) => ({ message, type })))
|
||||
)
|
||||
expect(await extensionHost.app.activeWindow!.showMessage('a')).toBe(null)
|
||||
expect(await extensionAPI.app.activeWindow!.showMessage('a')).toBe(undefined)
|
||||
expect(values).toEqual([{ message: 'a', type: MessageType.Info }] as typeof values)
|
||||
})
|
||||
|
||||
test('Window#showInputBox', async () => {
|
||||
const { extensionHost, services } = await integrationTestContext()
|
||||
const { extensionAPI, services } = await integrationTestContext()
|
||||
services.notifications.showInputs.subscribe(({ resolve }) => resolve(Promise.resolve('c')))
|
||||
const values = collectSubscribableValues(
|
||||
services.notifications.showInputs.pipe(map(({ message, defaultValue }) => ({ message, defaultValue })))
|
||||
)
|
||||
expect(await extensionHost.app.activeWindow!.showInputBox({ prompt: 'a', value: 'b' })).toBe('c')
|
||||
expect(await extensionAPI.app.activeWindow!.showInputBox({ prompt: 'a', value: 'b' })).toBe('c')
|
||||
expect(values).toEqual([{ message: 'a', defaultValue: 'b' }] as typeof values)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,581 +0,0 @@
|
||||
import { AbortController } from 'abort-controller'
|
||||
import { Observable, of } from 'rxjs'
|
||||
import { bufferCount, delay } from 'rxjs/operators'
|
||||
import { createBarrier } from '../../integration-test/testHelpers'
|
||||
import { createConnection } from './connection'
|
||||
import { ErrorCodes } from './messages'
|
||||
import { createMessagePipe, createMessageTransports } from './testHelpers'
|
||||
|
||||
describe('Connection', () => {
|
||||
// Polyfill
|
||||
;(global as any).AbortController = AbortController
|
||||
|
||||
test('handle single request', async () => {
|
||||
const method = 'test/handleSingleRequest'
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest(method, (p1, _signal) => {
|
||||
expect(p1).toEqual(['foo'])
|
||||
return p1
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
await expect(client.sendRequest(method, ['foo'])).resolves.toEqual(['foo'])
|
||||
})
|
||||
|
||||
test('handle single request with async result', async () => {
|
||||
const method = 'test/handleSingleRequest'
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest(method, (p1, _signal) => {
|
||||
expect(p1).toEqual(['foo'])
|
||||
return Promise.resolve(p1)
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
await expect(client.sendRequest(method, ['foo'])).resolves.toEqual(['foo'])
|
||||
})
|
||||
|
||||
test('abort undispatched request', async () => {
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
const b1 = createBarrier()
|
||||
const b2 = createBarrier()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest('block', async () => {
|
||||
b2.done()
|
||||
await b1.wait
|
||||
})
|
||||
server.onRequest('undispatched', () => {
|
||||
throw new Error('handler should not be called')
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
client.sendRequest('block').catch(null)
|
||||
await b2.wait
|
||||
const abortController = new AbortController()
|
||||
const result = client.sendRequest('undispatched', ['foo'], abortController.signal)
|
||||
abortController.abort()
|
||||
b1.done()
|
||||
await expect(result).rejects.toHaveProperty('name', 'AbortError')
|
||||
})
|
||||
|
||||
test('abort request currently being handled', async () => {
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest('m', (_params, signal) => {
|
||||
if (!signal) {
|
||||
throw new Error('!signal')
|
||||
}
|
||||
return new Promise<number>(resolve => {
|
||||
signal.addEventListener('abort', () => resolve(123))
|
||||
})
|
||||
})
|
||||
server.onRequest('ping', () => 'pong')
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
const abortController = new AbortController()
|
||||
const result = client.sendRequest('m', undefined, abortController.signal)
|
||||
await expect(client.sendRequest('ping')).resolves.toBe('pong') // waits until the 'm' message starts to be handled
|
||||
abortController.abort()
|
||||
await expect(result).rejects.toHaveProperty('name', 'AbortError')
|
||||
})
|
||||
|
||||
test('send request with single observable emission', async () => {
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest('m', (params: [number]) => of(params[0] + 1).pipe(delay(0)))
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
await expect(client.sendRequest<number>('m', [1])).resolves.toBe(2)
|
||||
})
|
||||
|
||||
test('observe request with single observable emission', async () => {
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest('m', (params: [number]) => of(params[0] + 1))
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
const result = client.observeRequest<number>('m', [1])
|
||||
await expect(result.toPromise()).resolves.toEqual(2)
|
||||
})
|
||||
|
||||
test('observe request with multiple observable emissions', async () => {
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest(
|
||||
'm',
|
||||
(params: number[]) =>
|
||||
new Observable<number>(observer => {
|
||||
for (const v of params) {
|
||||
observer.next(v + 1)
|
||||
}
|
||||
observer.complete()
|
||||
})
|
||||
)
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
await expect(
|
||||
client
|
||||
.observeRequest<number>('m', [1, 2, 3, 4])
|
||||
.pipe(bufferCount(4))
|
||||
.toPromise()
|
||||
).resolves.toEqual([2, 3, 4, 5])
|
||||
})
|
||||
|
||||
test('abort request before it is handled', async () => {
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
const server = createConnection(serverTransports)
|
||||
let handled = false
|
||||
server.onRequest('m', () => (handled = true))
|
||||
server.listen()
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
// Connection processes messages asynchronously.
|
||||
// When calling observeRequest() and unsubscribing from
|
||||
// the returned observable in the same tick,
|
||||
// the request handler is never called.
|
||||
const subscription = client.observeRequest('m').subscribe()
|
||||
subscription.unsubscribe()
|
||||
expect(handled).toBe(false)
|
||||
})
|
||||
|
||||
test('abort request with observable emission', async () => {
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
const server = createConnection(serverTransports)
|
||||
const handlerCalled = createBarrier()
|
||||
const handlerUnsubscribed = createBarrier()
|
||||
server.onRequest(
|
||||
'm',
|
||||
() =>
|
||||
new Observable<void>(() => {
|
||||
handlerCalled.done()
|
||||
return handlerUnsubscribed.done
|
||||
})
|
||||
)
|
||||
server.listen()
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
const subscription = client.observeRequest('m').subscribe()
|
||||
// Connection processes messages asynchronously.
|
||||
// wait until the request handler gets subscribed to server-side
|
||||
// to unsubscribe client-side.
|
||||
await handlerCalled.wait
|
||||
subscription.unsubscribe()
|
||||
await handlerUnsubscribed.wait
|
||||
})
|
||||
|
||||
test('handle multiple requests', async () => {
|
||||
const method = 'test/handleSingleRequest'
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest(method, (p1, _signal) => p1)
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
const promises: Promise<string>[] = []
|
||||
promises.push(client.sendRequest(method, ['foo']))
|
||||
promises.push(client.sendRequest(method, ['bar']))
|
||||
|
||||
await expect(Promise.all(promises)).resolves.toEqual([['foo'], ['bar']])
|
||||
})
|
||||
|
||||
test('unhandled request', async () => {
|
||||
const method = 'test/handleSingleRequest'
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
await expect(client.sendRequest(method, ['foo'])).rejects.toHaveProperty('code', ErrorCodes.MethodNotFound)
|
||||
})
|
||||
|
||||
test('handler throws an Error', async () => {
|
||||
const method = 'test/handleSingleRequest'
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest(method, () => {
|
||||
throw new Error('test')
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
const result = client.sendRequest(method, ['foo'])
|
||||
await expect(result).rejects.toMatchObject({ code: ErrorCodes.InternalError, message: 'test' })
|
||||
await expect(result).rejects.toHaveProperty('data')
|
||||
await expect(result.catch(err => typeof err.data.stack)).resolves.toBe('string')
|
||||
})
|
||||
|
||||
test('handler returns a rejected Promise with an Error', async () => {
|
||||
const method = 'test/handleSingleRequest'
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest(method, () => Promise.reject(new Error('test')))
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
const result = client.sendRequest(method, ['foo'])
|
||||
await expect(result).rejects.toMatchObject({ code: ErrorCodes.InternalError, message: 'test' })
|
||||
await expect(result).rejects.toHaveProperty('data')
|
||||
await expect(result.catch(err => typeof err.data.stack)).resolves.toBe('string')
|
||||
})
|
||||
|
||||
test('receives undefined request params as null', async () => {
|
||||
const method = 'test/handleSingleRequest'
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest(method, params => {
|
||||
expect(params).toBe(null)
|
||||
return ''
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
await client.sendRequest(method)
|
||||
})
|
||||
|
||||
test('receives undefined notification params as null', async () => {
|
||||
const method = 'testNotification'
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onNotification(method, params => {
|
||||
expect(params).toBe(null)
|
||||
return ''
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
client.sendNotification(method)
|
||||
})
|
||||
|
||||
test('receives null as null', async () => {
|
||||
const method = 'test/handleSingleRequest'
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest(method, params => {
|
||||
expect(params).toEqual([null])
|
||||
return null
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
await expect(client.sendRequest(method, [null])).resolves.toBe(null)
|
||||
})
|
||||
|
||||
test('receives 0 as 0', async () => {
|
||||
const method = 'test/handleSingleRequest'
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest(method, params => {
|
||||
expect(params).toEqual([0])
|
||||
return 0
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
await expect(client.sendRequest(method, [0])).resolves.toBe(0)
|
||||
})
|
||||
|
||||
const testNotification = 'testNotification'
|
||||
test('sends and receives notification', done => {
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onNotification(testNotification, params => {
|
||||
expect(params).toEqual([{ value: true }])
|
||||
done()
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
client.sendNotification(testNotification, [{ value: true }])
|
||||
})
|
||||
|
||||
test('unhandled notification event', done => {
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onUnhandledNotification(message => {
|
||||
expect(message.method).toBe(testNotification)
|
||||
expect(message.params).toEqual([{ value: true }])
|
||||
done()
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
client.sendNotification(testNotification, [{ value: true }])
|
||||
})
|
||||
|
||||
test('unsubscribes client connection', async () => {
|
||||
const method = 'test/handleSingleRequest'
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest(method, _param => {
|
||||
client.unsubscribe()
|
||||
return ''
|
||||
})
|
||||
server.listen()
|
||||
|
||||
client.listen()
|
||||
await expect(client.sendRequest(method, [''])).rejects.toMatchObject({
|
||||
message:
|
||||
'The underlying JSON-RPC connection got unsubscribed while responding to this test/handleSingleRequest request.',
|
||||
})
|
||||
server.unsubscribe()
|
||||
})
|
||||
|
||||
test('unsubscribed connection throws', () => {
|
||||
const client = createConnection(createMessagePipe())
|
||||
client.listen()
|
||||
client.unsubscribe()
|
||||
expect(() => client.sendNotification(testNotification)).toThrow()
|
||||
})
|
||||
|
||||
test('two listen throw', () => {
|
||||
const client = createConnection(createMessagePipe())
|
||||
client.listen()
|
||||
expect(() => client.listen()).toThrow()
|
||||
})
|
||||
|
||||
test('notify on connection unsubscribe', done => {
|
||||
const client = createConnection(createMessagePipe())
|
||||
client.listen()
|
||||
client.onUnsubscribe(() => {
|
||||
done()
|
||||
})
|
||||
client.unsubscribe()
|
||||
})
|
||||
|
||||
test('params in notifications', done => {
|
||||
const method = 'test'
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onNotification(method, params => {
|
||||
expect(params).toEqual([10, 'vscode'])
|
||||
done()
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
client.sendNotification(method, [10, 'vscode'])
|
||||
})
|
||||
|
||||
test('params in request/response', async () => {
|
||||
const method = 'add'
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest(method, (params: number[]) => {
|
||||
expect(params).toEqual([10, 20, 30])
|
||||
return params.reduce((sum, n) => sum + n, 0)
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
await expect(client.sendRequest(method, [10, 20, 30])).resolves.toBe(60)
|
||||
})
|
||||
|
||||
test('params in request/response with signal', async () => {
|
||||
const method = 'add'
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest(method, (params: number[], _signal) => {
|
||||
expect(params).toEqual([10, 20, 30])
|
||||
return params.reduce((sum, n) => sum + n, 0)
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
const signal = new AbortController().signal
|
||||
client.listen()
|
||||
await expect(client.sendRequest(method, [10, 20, 30], signal)).resolves.toBe(60)
|
||||
})
|
||||
|
||||
test('1 param as array in request', async () => {
|
||||
const type = 'add'
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest(type, p1 => {
|
||||
expect(Array.isArray(p1)).toBeTruthy()
|
||||
expect(p1[0]).toBe(10)
|
||||
expect(p1[1]).toBe(20)
|
||||
expect(p1[2]).toBe(30)
|
||||
return 60
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
const signal = new AbortController().signal
|
||||
client.listen()
|
||||
await expect(client.sendRequest(type, [10, 20, 30], signal)).resolves.toBe(60)
|
||||
})
|
||||
|
||||
test('1 param as array in notification', done => {
|
||||
const type = 'add'
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onNotification(type, params => {
|
||||
expect(params).toEqual([10, 20, 30])
|
||||
done()
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
client.sendNotification(type, [10, 20, 30])
|
||||
})
|
||||
|
||||
test('untyped request/response', async () => {
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest('test', (params: number[], _signal) => {
|
||||
expect(params).toEqual([10, 20, 30])
|
||||
return params.reduce((sum, n) => sum + n, 0)
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
const signal = new AbortController().signal
|
||||
client.listen()
|
||||
await expect(client.sendRequest('test', [10, 20, 30], signal)).resolves.toBe(60)
|
||||
})
|
||||
|
||||
test('untyped notification', done => {
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onNotification('test', (params: number[]) => {
|
||||
expect(params).toEqual([10, 20, 30])
|
||||
done()
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
client.sendNotification('test', [10, 20, 30])
|
||||
})
|
||||
|
||||
test('star request handler', async () => {
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest((method: string, params: number[], _signal) => {
|
||||
expect(method).toBe('test')
|
||||
expect(params).toEqual([10, 20, 30])
|
||||
return params.reduce((sum, n) => sum + n, 0)
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
const signal = new AbortController().signal
|
||||
client.listen()
|
||||
await expect(client.sendRequest('test', [10, 20, 30], signal)).resolves.toBe(60)
|
||||
})
|
||||
|
||||
test('star notification handler', done => {
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onNotification((method: string, params: number[]) => {
|
||||
expect(method).toBe('test')
|
||||
expect(params).toEqual([10, 20, 30])
|
||||
done()
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
client.sendNotification('test', [10, 20, 30])
|
||||
})
|
||||
|
||||
test('abort signal is undefined', async () => {
|
||||
const type = 'add'
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest(type, (params: number[], _signal) => {
|
||||
expect(params).toEqual([10, 20, 30])
|
||||
return params.reduce((sum, n) => sum + n, 0)
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
await expect(client.sendRequest(type, [10, 20, 30], undefined)).resolves.toBe(60)
|
||||
})
|
||||
|
||||
test('null params in request', async () => {
|
||||
const type = 'add'
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onRequest(type, _signal => 123)
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
;(client.sendRequest as any)(type, null).then((result: any) => expect(result).toBe(123))
|
||||
})
|
||||
|
||||
test('null params in notifications', done => {
|
||||
const type = 'test'
|
||||
const [serverTransports, clientTransports] = createMessageTransports()
|
||||
|
||||
const server = createConnection(serverTransports)
|
||||
server.onNotification(type, params => {
|
||||
expect(params).toBe(null)
|
||||
done()
|
||||
})
|
||||
server.listen()
|
||||
|
||||
const client = createConnection(clientTransports)
|
||||
client.listen()
|
||||
;(client.sendNotification as any)(type, null)
|
||||
})
|
||||
})
|
||||
@ -1,719 +0,0 @@
|
||||
import { toPromise } from 'abortable-rx'
|
||||
import { from, fromEvent, isObservable, Observable, Observer, Subject, Unsubscribable } from 'rxjs'
|
||||
import { takeUntil } from 'rxjs/operators'
|
||||
import { isPromise } from '../../util'
|
||||
import { Emitter, Event } from './events'
|
||||
import { LinkedMap } from './linkedMap'
|
||||
import {
|
||||
ErrorCodes,
|
||||
isNotificationMessage,
|
||||
isRequestMessage,
|
||||
isResponseMessage,
|
||||
Message,
|
||||
NotificationMessage,
|
||||
RequestID,
|
||||
RequestMessage,
|
||||
ResponseError,
|
||||
ResponseMessage,
|
||||
} from './messages'
|
||||
import { noopTracer, Tracer } from './trace'
|
||||
import { DataCallback, MessageReader, MessageWriter } from './transport'
|
||||
|
||||
// Copied from vscode-languageserver to avoid adding extraneous dependencies.
|
||||
|
||||
export interface Logger {
|
||||
error(message: string): void
|
||||
warn(message: string): void
|
||||
info(message: string): void
|
||||
log(message: string): void
|
||||
}
|
||||
|
||||
const NullLogger: Logger = Object.freeze({
|
||||
error: () => {
|
||||
/* noop */
|
||||
},
|
||||
warn: () => {
|
||||
/* noop */
|
||||
},
|
||||
info: () => {
|
||||
/* noop */
|
||||
},
|
||||
log: () => {
|
||||
/* noop */
|
||||
},
|
||||
})
|
||||
|
||||
export enum ConnectionErrors {
|
||||
/**
|
||||
* The connection is closed.
|
||||
*/
|
||||
Closed = 1,
|
||||
/**
|
||||
* The connection got unsubscribed (i.e., disposed).
|
||||
*/
|
||||
Unsubscribed = 2,
|
||||
/**
|
||||
* The connection is already in listening mode.
|
||||
*/
|
||||
AlreadyListening = 3,
|
||||
}
|
||||
|
||||
export class ConnectionError extends Error {
|
||||
public readonly code: ConnectionErrors
|
||||
|
||||
constructor(code: ConnectionErrors, message: string) {
|
||||
super(message)
|
||||
this.code = code
|
||||
Object.setPrototypeOf(this, ConnectionError.prototype)
|
||||
}
|
||||
}
|
||||
|
||||
type MessageQueue = LinkedMap<string, Message>
|
||||
|
||||
type HandlerResult<R, E> =
|
||||
| R
|
||||
| ResponseError<E>
|
||||
| Promise<R>
|
||||
| Promise<ResponseError<E>>
|
||||
| Promise<R | ResponseError<E>>
|
||||
| Observable<R>
|
||||
|
||||
type StarRequestHandler = (method: string, params?: any, signal?: AbortSignal) => HandlerResult<any, any>
|
||||
|
||||
type GenericRequestHandler<R, E> = (params?: any, signal?: AbortSignal) => HandlerResult<R, E>
|
||||
|
||||
type StarNotificationHandler = (method: string, params?: any) => void
|
||||
|
||||
type GenericNotificationHandler = (params: any) => void
|
||||
|
||||
export interface Connection extends Unsubscribable {
|
||||
sendRequest<R>(method: string, params?: any[], signal?: AbortSignal): Promise<R>
|
||||
observeRequest<R>(method: string, params?: any[]): Observable<R>
|
||||
|
||||
onRequest<R, E>(method: string, handler: GenericRequestHandler<R, E>): void
|
||||
onRequest(handler: StarRequestHandler): void
|
||||
|
||||
sendNotification(method: string, params?: any[]): void
|
||||
|
||||
onNotification(method: string, handler: GenericNotificationHandler): void
|
||||
onNotification(handler: StarNotificationHandler): void
|
||||
|
||||
trace(tracer: Tracer | null): void
|
||||
|
||||
onError: Event<[Error, Message | undefined, number | undefined]>
|
||||
onClose: Event<void>
|
||||
onUnhandledNotification: Event<NotificationMessage>
|
||||
listen(): void
|
||||
onUnsubscribe: Event<void>
|
||||
}
|
||||
|
||||
export interface MessageTransports {
|
||||
reader: MessageReader
|
||||
writer: MessageWriter
|
||||
}
|
||||
|
||||
export function createConnection(transports: MessageTransports, logger?: Logger): Connection {
|
||||
if (!logger) {
|
||||
logger = NullLogger
|
||||
}
|
||||
return _createConnection(transports, logger)
|
||||
}
|
||||
|
||||
interface ResponseObserver {
|
||||
/** The request's method. */
|
||||
method: string
|
||||
|
||||
/** Only set in Trace.Verbose mode. */
|
||||
request?: RequestMessage
|
||||
|
||||
/** The timestamp when the request was received. */
|
||||
timerStart: number
|
||||
|
||||
/** Whether the request was aborted by the client. */
|
||||
complete: boolean
|
||||
|
||||
/** The observable containing the result value(s) and state. */
|
||||
observer: Observer<any>
|
||||
}
|
||||
|
||||
enum ConnectionState {
|
||||
New = 1,
|
||||
Listening = 2,
|
||||
Closed = 3,
|
||||
Unsubscribed = 4,
|
||||
}
|
||||
|
||||
interface RequestHandlerElement {
|
||||
type: string | undefined
|
||||
handler: GenericRequestHandler<any, any>
|
||||
}
|
||||
|
||||
interface NotificationHandlerElement {
|
||||
type: string | undefined
|
||||
handler: GenericNotificationHandler
|
||||
}
|
||||
|
||||
const ABORT_REQUEST_METHOD = '$/abortRequest'
|
||||
|
||||
function _createConnection(transports: MessageTransports, logger: Logger): Connection {
|
||||
let sequenceNumber = 0
|
||||
let notificationSquenceNumber = 0
|
||||
let unknownResponseSquenceNumber = 0
|
||||
const version = '2.0'
|
||||
|
||||
let starRequestHandler: StarRequestHandler | undefined
|
||||
const requestHandlers: { [name: string]: RequestHandlerElement | undefined } = Object.create(null)
|
||||
let starNotificationHandler: StarNotificationHandler | undefined
|
||||
const notificationHandlers: { [name: string]: NotificationHandlerElement | undefined } = Object.create(null)
|
||||
|
||||
let timer = false
|
||||
let messageQueue: MessageQueue = new LinkedMap<string, Message>()
|
||||
let responseObservables: { [name: string]: ResponseObserver } = Object.create(null)
|
||||
let requestAbortControllers: { [id: string]: AbortController } = Object.create(null)
|
||||
|
||||
let tracer: Tracer = noopTracer
|
||||
|
||||
let state: ConnectionState = ConnectionState.New
|
||||
const errorEmitter = new Emitter<[Error, Message | undefined, number | undefined]>()
|
||||
const closeEmitter: Emitter<void> = new Emitter<void>()
|
||||
const unhandledNotificationEmitter: Emitter<NotificationMessage> = new Emitter<NotificationMessage>()
|
||||
|
||||
const unsubscribeEmitter: Emitter<void> = new Emitter<void>()
|
||||
|
||||
function createRequestQueueKey(id: string | number): string {
|
||||
return 'req-' + id.toString()
|
||||
}
|
||||
|
||||
function createResponseQueueKey(id: string | number | null): string {
|
||||
if (id === null) {
|
||||
return 'res-unknown-' + (++unknownResponseSquenceNumber).toString()
|
||||
} else {
|
||||
return 'res-' + id.toString()
|
||||
}
|
||||
}
|
||||
|
||||
function createNotificationQueueKey(): string {
|
||||
return 'not-' + (++notificationSquenceNumber).toString()
|
||||
}
|
||||
|
||||
function addMessageToQueue(queue: MessageQueue, message: Message): void {
|
||||
if (isRequestMessage(message)) {
|
||||
queue.set(createRequestQueueKey(message.id), message)
|
||||
} else if (isResponseMessage(message)) {
|
||||
const key = createResponseQueueKey(message.id) + Math.random() // TODO(sqs)
|
||||
queue.set(key, message)
|
||||
} else {
|
||||
queue.set(createNotificationQueueKey(), message)
|
||||
}
|
||||
}
|
||||
|
||||
function isListening(): boolean {
|
||||
return state === ConnectionState.Listening
|
||||
}
|
||||
|
||||
function isClosed(): boolean {
|
||||
return state === ConnectionState.Closed
|
||||
}
|
||||
|
||||
function isUnsubscribed(): boolean {
|
||||
return state === ConnectionState.Unsubscribed
|
||||
}
|
||||
|
||||
function closeHandler(): void {
|
||||
if (state === ConnectionState.New || state === ConnectionState.Listening) {
|
||||
state = ConnectionState.Closed
|
||||
closeEmitter.fire(undefined)
|
||||
}
|
||||
// If the connection is unsubscribed don't sent close events.
|
||||
}
|
||||
|
||||
function readErrorHandler(error: Error): void {
|
||||
errorEmitter.fire([error, undefined, undefined])
|
||||
}
|
||||
|
||||
function writeErrorHandler(data: [Error, Message | undefined, number | undefined]): void {
|
||||
errorEmitter.fire(data)
|
||||
}
|
||||
|
||||
transports.reader.onClose(closeHandler)
|
||||
transports.reader.onError(readErrorHandler)
|
||||
|
||||
transports.writer.onClose(closeHandler)
|
||||
transports.writer.onError(writeErrorHandler)
|
||||
|
||||
function triggerMessageQueue(): void {
|
||||
if (timer || messageQueue.size === 0) {
|
||||
return
|
||||
}
|
||||
timer = true
|
||||
setImmediateCompat(() => {
|
||||
timer = false
|
||||
processMessageQueue()
|
||||
})
|
||||
}
|
||||
|
||||
function processMessageQueue(): void {
|
||||
if (messageQueue.size === 0) {
|
||||
return
|
||||
}
|
||||
const message = messageQueue.shift()!
|
||||
try {
|
||||
if (isRequestMessage(message)) {
|
||||
handleRequest(message)
|
||||
} else if (isNotificationMessage(message)) {
|
||||
handleNotification(message)
|
||||
} else if (isResponseMessage(message)) {
|
||||
handleResponse(message)
|
||||
} else {
|
||||
handleInvalidMessage(message)
|
||||
}
|
||||
} finally {
|
||||
triggerMessageQueue()
|
||||
}
|
||||
}
|
||||
|
||||
const callback: DataCallback = message => {
|
||||
try {
|
||||
// We have received an abort signal. Check if the message is still in the queue and abort it if allowed
|
||||
// to do so.
|
||||
if (isNotificationMessage(message) && message.method === ABORT_REQUEST_METHOD) {
|
||||
const key = createRequestQueueKey(message.params[0])
|
||||
const toAbort = messageQueue.get(key)
|
||||
if (isRequestMessage(toAbort)) {
|
||||
messageQueue.delete(key)
|
||||
const response: ResponseMessage = {
|
||||
jsonrpc: '2.0',
|
||||
id: toAbort.id,
|
||||
error: { code: ErrorCodes.RequestAborted, message: 'request aborted' },
|
||||
}
|
||||
tracer.responseAborted(response, toAbort, message)
|
||||
transports.writer.write(response)
|
||||
return
|
||||
}
|
||||
}
|
||||
addMessageToQueue(messageQueue, message)
|
||||
} finally {
|
||||
triggerMessageQueue()
|
||||
}
|
||||
}
|
||||
|
||||
function handleRequest(requestMessage: RequestMessage): void {
|
||||
if (isUnsubscribed()) {
|
||||
// we return here silently since we fired an event when the
|
||||
// connection got unsubscribed.
|
||||
return
|
||||
}
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
function reply(resultOrError: any | ResponseError<any>, complete: boolean): void {
|
||||
const message: ResponseMessage = {
|
||||
jsonrpc: version,
|
||||
id: requestMessage.id,
|
||||
complete,
|
||||
}
|
||||
if (resultOrError instanceof ResponseError) {
|
||||
message.error = (resultOrError as ResponseError<any>).toJSON()
|
||||
} else {
|
||||
message.result = resultOrError === undefined ? null : resultOrError
|
||||
}
|
||||
tracer.responseSent(message, requestMessage, startTime)
|
||||
transports.writer.write(message)
|
||||
}
|
||||
function replyError(error: ResponseError<any>): void {
|
||||
const message: ResponseMessage = {
|
||||
jsonrpc: version,
|
||||
id: requestMessage.id,
|
||||
error: error.toJSON(),
|
||||
complete: true,
|
||||
}
|
||||
tracer.responseSent(message, requestMessage, startTime)
|
||||
transports.writer.write(message)
|
||||
}
|
||||
function replySuccess(result: any): void {
|
||||
// The JSON RPC defines that a response must either have a result or an error
|
||||
// So we can't treat undefined as a valid response result.
|
||||
if (result === undefined) {
|
||||
result = null
|
||||
}
|
||||
const message: ResponseMessage = {
|
||||
jsonrpc: version,
|
||||
id: requestMessage.id,
|
||||
result,
|
||||
complete: true,
|
||||
}
|
||||
tracer.responseSent(message, requestMessage, startTime)
|
||||
transports.writer.write(message)
|
||||
}
|
||||
function replyComplete(): void {
|
||||
const message: ResponseMessage = {
|
||||
jsonrpc: version,
|
||||
id: requestMessage.id,
|
||||
complete: true,
|
||||
}
|
||||
tracer.responseSent(message, requestMessage, startTime)
|
||||
transports.writer.write(message)
|
||||
}
|
||||
|
||||
tracer.requestReceived(requestMessage)
|
||||
|
||||
const element = requestHandlers[requestMessage.method]
|
||||
const requestHandler: GenericRequestHandler<any, any> | undefined = element && element.handler
|
||||
if (requestHandler || starRequestHandler) {
|
||||
const abortController = new AbortController()
|
||||
const signalKey = String(requestMessage.id)
|
||||
requestAbortControllers[signalKey] = abortController
|
||||
try {
|
||||
const params = requestMessage.params !== undefined ? requestMessage.params : null
|
||||
const handlerResult = requestHandler
|
||||
? requestHandler(params, abortController.signal)
|
||||
: starRequestHandler!(requestMessage.method, params, abortController.signal)
|
||||
|
||||
if (!handlerResult) {
|
||||
delete requestAbortControllers[signalKey]
|
||||
replySuccess(handlerResult)
|
||||
} else if (isPromise(handlerResult) || isObservable(handlerResult)) {
|
||||
const onComplete = () => {
|
||||
delete requestAbortControllers[signalKey]
|
||||
}
|
||||
from(handlerResult)
|
||||
.pipe(takeUntil(fromEvent(abortController.signal, 'abort')))
|
||||
.subscribe(
|
||||
value => reply(value, false),
|
||||
error => {
|
||||
onComplete()
|
||||
if (error instanceof ResponseError) {
|
||||
replyError(error as ResponseError<any>)
|
||||
} else if (error && typeof error.message === 'string') {
|
||||
replyError(
|
||||
new ResponseError<any>(ErrorCodes.InternalError, error.message, {
|
||||
stack: error.stack,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
replyError(
|
||||
new ResponseError<void>(
|
||||
ErrorCodes.InternalError,
|
||||
`Request ${
|
||||
requestMessage.method
|
||||
} failed unexpectedly without providing any details.`
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
() => {
|
||||
onComplete()
|
||||
replyComplete()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
delete requestAbortControllers[signalKey]
|
||||
reply(handlerResult, true)
|
||||
}
|
||||
} catch (error) {
|
||||
delete requestAbortControllers[signalKey]
|
||||
if (error instanceof ResponseError) {
|
||||
reply(error as ResponseError<any>, true)
|
||||
} else if (error && typeof error.message === 'string') {
|
||||
replyError(
|
||||
new ResponseError<any>(ErrorCodes.InternalError, error.message, {
|
||||
stack: error.stack,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
replyError(
|
||||
new ResponseError<void>(
|
||||
ErrorCodes.InternalError,
|
||||
`Request ${requestMessage.method} failed unexpectedly without providing any details.`
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
replyError(new ResponseError<void>(ErrorCodes.MethodNotFound, `Unhandled method ${requestMessage.method}`))
|
||||
}
|
||||
}
|
||||
|
||||
function handleResponse(responseMessage: ResponseMessage): void {
|
||||
if (isUnsubscribed()) {
|
||||
// See handle request.
|
||||
return
|
||||
}
|
||||
|
||||
if (responseMessage.id === null) {
|
||||
if (responseMessage.error) {
|
||||
logger.error(
|
||||
`Received response message without id: Error is: \n${JSON.stringify(
|
||||
responseMessage.error,
|
||||
undefined,
|
||||
4
|
||||
)}`
|
||||
)
|
||||
} else {
|
||||
logger.error(`Received response message without id. No further error information provided.`)
|
||||
}
|
||||
} else {
|
||||
const key = String(responseMessage.id)
|
||||
const responseObservable = responseObservables[key]
|
||||
if (responseObservable) {
|
||||
tracer.responseReceived(
|
||||
responseMessage,
|
||||
responseObservable.request || responseObservable.method,
|
||||
responseObservable.timerStart
|
||||
)
|
||||
try {
|
||||
if (responseMessage.error) {
|
||||
const { code, message, data } = responseMessage.error
|
||||
const err = new ResponseError(code, message, data)
|
||||
if (data && data.stack) {
|
||||
err.stack = data.stack
|
||||
}
|
||||
responseObservable.observer.error(err)
|
||||
} else if (responseMessage.result !== undefined) {
|
||||
responseObservable.observer.next(responseMessage.result)
|
||||
}
|
||||
if (responseMessage.complete) {
|
||||
responseObservable.complete = true
|
||||
responseObservable.observer.complete()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.message) {
|
||||
logger.error(
|
||||
`Response handler '${responseObservable.method}' failed with message: ${error.message}`
|
||||
)
|
||||
} else {
|
||||
logger.error(`Response handler '${responseObservable.method}' failed unexpectedly.`)
|
||||
}
|
||||
} finally {
|
||||
if (responseMessage.complete) {
|
||||
delete responseObservables[key]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracer.unknownResponseReceived(responseMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleNotification(message: NotificationMessage): void {
|
||||
if (isUnsubscribed()) {
|
||||
// See handle request.
|
||||
return
|
||||
}
|
||||
let notificationHandler: GenericNotificationHandler | undefined
|
||||
if (message.method === ABORT_REQUEST_METHOD) {
|
||||
notificationHandler = (params: [RequestID]) => {
|
||||
const id = params[0]
|
||||
const abortController = requestAbortControllers[String(id)]
|
||||
if (abortController) {
|
||||
abortController.abort()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const element = notificationHandlers[message.method]
|
||||
if (element) {
|
||||
notificationHandler = element.handler
|
||||
}
|
||||
}
|
||||
if (notificationHandler || starNotificationHandler) {
|
||||
try {
|
||||
tracer.notificationReceived(message)
|
||||
notificationHandler
|
||||
? notificationHandler(message.params)
|
||||
: starNotificationHandler!(message.method, message.params)
|
||||
} catch (error) {
|
||||
if (error.message) {
|
||||
logger.error(`Notification handler '${message.method}' failed with message: ${error.message}`)
|
||||
} else {
|
||||
logger.error(`Notification handler '${message.method}' failed unexpectedly.`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unhandledNotificationEmitter.fire(message)
|
||||
}
|
||||
}
|
||||
|
||||
function handleInvalidMessage(message: Message): void {
|
||||
if (!message) {
|
||||
logger.error('Received empty message.')
|
||||
return
|
||||
}
|
||||
logger.error(
|
||||
`Received message which is neither a response nor a notification message:\n${JSON.stringify(
|
||||
message,
|
||||
null,
|
||||
4
|
||||
)}`
|
||||
)
|
||||
// Test whether we find an id to reject the promise
|
||||
const responseMessage: ResponseMessage = message as ResponseMessage
|
||||
if (typeof responseMessage.id === 'string' || typeof responseMessage.id === 'number') {
|
||||
const key = String(responseMessage.id)
|
||||
const responseHandler = responseObservables[key]
|
||||
if (responseHandler) {
|
||||
responseHandler.observer.error(
|
||||
new Error('The received response has neither a result nor an error property.')
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function throwIfClosedOrUnsubscribed(): void {
|
||||
if (isClosed()) {
|
||||
throw new ConnectionError(
|
||||
ConnectionErrors.Closed,
|
||||
'Extension host connection unexpectedly closed. Reload the page to resolve.'
|
||||
)
|
||||
}
|
||||
if (isUnsubscribed()) {
|
||||
throw new ConnectionError(ConnectionErrors.Unsubscribed, 'Connection is unsubscribed.')
|
||||
}
|
||||
}
|
||||
|
||||
function throwIfListening(): void {
|
||||
if (isListening()) {
|
||||
throw new ConnectionError(ConnectionErrors.AlreadyListening, 'Connection is already listening')
|
||||
}
|
||||
}
|
||||
|
||||
function throwIfNotListening(): void {
|
||||
if (!isListening()) {
|
||||
throw new Error('Call listen() first.')
|
||||
}
|
||||
}
|
||||
|
||||
const sendNotification = (method: string, params?: any[]): void => {
|
||||
throwIfClosedOrUnsubscribed()
|
||||
const notificationMessage: NotificationMessage = {
|
||||
jsonrpc: version,
|
||||
method,
|
||||
params,
|
||||
}
|
||||
tracer.notificationSent(notificationMessage)
|
||||
transports.writer.write(notificationMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a hot observable with the result of sending the request
|
||||
*/
|
||||
const requestHelper = <R>(method: string, params?: any[]): Observable<R> => {
|
||||
const id = sequenceNumber++
|
||||
const requestMessage: RequestMessage = {
|
||||
jsonrpc: version,
|
||||
id,
|
||||
method,
|
||||
params,
|
||||
}
|
||||
const subject = new Subject<R>()
|
||||
const responseObserver: ResponseObserver = {
|
||||
method,
|
||||
request: requestMessage,
|
||||
timerStart: Date.now(),
|
||||
complete: false,
|
||||
observer: subject,
|
||||
}
|
||||
tracer.requestSent(requestMessage)
|
||||
try {
|
||||
transports.writer.write(requestMessage)
|
||||
responseObservables[String(id)] = responseObserver
|
||||
} catch (e) {
|
||||
responseObserver.observer.error(
|
||||
new ResponseError<void>(ErrorCodes.MessageWriteError, e.message ? e.message : 'Unknown reason')
|
||||
)
|
||||
}
|
||||
return new Observable(observer => {
|
||||
subject.subscribe(observer).add(() => {
|
||||
if (
|
||||
!isUnsubscribed() &&
|
||||
responseObserver &&
|
||||
!responseObserver.complete &&
|
||||
!responseObserver.observer.closed
|
||||
) {
|
||||
sendNotification(ABORT_REQUEST_METHOD, [id])
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const connection: Connection = {
|
||||
sendNotification,
|
||||
onNotification: (type: string | StarNotificationHandler, handler?: GenericNotificationHandler): void => {
|
||||
throwIfClosedOrUnsubscribed()
|
||||
if (typeof type === 'function') {
|
||||
starNotificationHandler = type
|
||||
} else if (handler) {
|
||||
notificationHandlers[type] = { type: undefined, handler }
|
||||
}
|
||||
},
|
||||
sendRequest: <R>(method: string, params?: any[], signal?: AbortSignal): Promise<R> => {
|
||||
throwIfClosedOrUnsubscribed()
|
||||
throwIfNotListening()
|
||||
return toPromise(requestHelper<R>(method, params), signal)
|
||||
},
|
||||
observeRequest: <R>(method: string, params?: any[]): Observable<R> => {
|
||||
throwIfClosedOrUnsubscribed()
|
||||
throwIfNotListening()
|
||||
return requestHelper<R>(method, params)
|
||||
},
|
||||
onRequest: <R, E>(type: string | StarRequestHandler, handler?: GenericRequestHandler<R, E>): void => {
|
||||
throwIfClosedOrUnsubscribed()
|
||||
|
||||
if (typeof type === 'function') {
|
||||
starRequestHandler = type
|
||||
} else if (handler) {
|
||||
requestHandlers[type] = { type: undefined, handler }
|
||||
}
|
||||
},
|
||||
trace: (_tracer: Tracer | null) => {
|
||||
if (_tracer) {
|
||||
tracer = _tracer
|
||||
} else {
|
||||
tracer = noopTracer
|
||||
}
|
||||
},
|
||||
onError: errorEmitter.event,
|
||||
onClose: closeEmitter.event,
|
||||
onUnhandledNotification: unhandledNotificationEmitter.event,
|
||||
onUnsubscribe: unsubscribeEmitter.event,
|
||||
unsubscribe: () => {
|
||||
if (isUnsubscribed()) {
|
||||
return
|
||||
}
|
||||
state = ConnectionState.Unsubscribed
|
||||
unsubscribeEmitter.fire(undefined)
|
||||
for (const key of Object.keys(responseObservables)) {
|
||||
responseObservables[key].observer.error(
|
||||
new ConnectionError(
|
||||
ConnectionErrors.Unsubscribed,
|
||||
`The underlying JSON-RPC connection got unsubscribed while responding to this ${
|
||||
responseObservables[key].method
|
||||
} request.`
|
||||
)
|
||||
)
|
||||
}
|
||||
responseObservables = Object.create(null)
|
||||
requestAbortControllers = Object.create(null)
|
||||
messageQueue = new LinkedMap<string, Message>()
|
||||
transports.writer.unsubscribe()
|
||||
transports.reader.unsubscribe()
|
||||
},
|
||||
listen: () => {
|
||||
throwIfClosedOrUnsubscribed()
|
||||
throwIfListening()
|
||||
|
||||
state = ConnectionState.Listening
|
||||
transports.reader.listen(callback)
|
||||
},
|
||||
}
|
||||
|
||||
return connection
|
||||
}
|
||||
|
||||
/** Support browser and node environments without needing a transpiler. */
|
||||
function setImmediateCompat(f: () => void): void {
|
||||
if (typeof setImmediate !== 'undefined') {
|
||||
setImmediate(f)
|
||||
return
|
||||
}
|
||||
setTimeout(f, 0)
|
||||
}
|
||||
@ -1,137 +0,0 @@
|
||||
import { Unsubscribable } from 'rxjs'
|
||||
|
||||
/**
|
||||
* Represents a typed event.
|
||||
*/
|
||||
export type Event<T> = (listener: (e: T) => any, thisArgs?: any) => Unsubscribable
|
||||
|
||||
export namespace Event {
|
||||
const _unsubscribable = {
|
||||
unsubscribe(): void {
|
||||
/* noop */
|
||||
},
|
||||
}
|
||||
export const None: Event<any> = () => _unsubscribable
|
||||
}
|
||||
|
||||
class CallbackList {
|
||||
private _callbacks: ((...args: any[]) => any)[] | undefined
|
||||
private _contexts: any[] | undefined
|
||||
|
||||
public add(callback: (...args: any[]) => any, context: any = null, bucket?: Unsubscribable[]): void {
|
||||
if (!this._callbacks) {
|
||||
this._callbacks = []
|
||||
this._contexts = []
|
||||
}
|
||||
this._callbacks.push(callback)
|
||||
this._contexts!.push(context)
|
||||
|
||||
if (Array.isArray(bucket)) {
|
||||
bucket.push({ unsubscribe: () => this.remove(callback, context) })
|
||||
}
|
||||
}
|
||||
|
||||
public remove(callback: (...args: any[]) => any, context: any = null): void {
|
||||
if (!this._callbacks) {
|
||||
return
|
||||
}
|
||||
|
||||
let foundCallbackWithDifferentContext = false
|
||||
for (let i = 0, len = this._callbacks.length; i < len; i++) {
|
||||
if (this._callbacks[i] === callback) {
|
||||
if (this._contexts![i] === context) {
|
||||
// callback & context match => remove it
|
||||
this._callbacks.splice(i, 1)
|
||||
this._contexts!.splice(i, 1)
|
||||
return
|
||||
} else {
|
||||
foundCallbackWithDifferentContext = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundCallbackWithDifferentContext) {
|
||||
throw new Error('When adding a listener with a context, you should remove it with the same context')
|
||||
}
|
||||
}
|
||||
|
||||
public invoke(...args: any[]): any[] {
|
||||
if (!this._callbacks) {
|
||||
return []
|
||||
}
|
||||
|
||||
const ret: any[] = []
|
||||
const callbacks = this._callbacks.slice(0)
|
||||
const contexts = this._contexts!.slice(0)
|
||||
for (let i = 0; i < callbacks.length; i++) {
|
||||
try {
|
||||
ret.push(callbacks[i].apply(contexts[i], args))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
public isEmpty(): boolean {
|
||||
return !this._callbacks || this._callbacks.length === 0
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
this._callbacks = undefined
|
||||
this._contexts = undefined
|
||||
}
|
||||
}
|
||||
|
||||
export class Emitter<T> {
|
||||
private static _noop = () => void 0
|
||||
|
||||
private _event?: Event<T>
|
||||
private _callbacks: CallbackList | undefined
|
||||
|
||||
/**
|
||||
* For the public to allow to subscribe
|
||||
* to events from this Emitter
|
||||
*/
|
||||
public get event(): Event<T> {
|
||||
if (!this._event) {
|
||||
this._event = (listener: (e: T) => any, thisArgs?: any, Unsubscribables?: Unsubscribable[]) => {
|
||||
if (!this._callbacks) {
|
||||
this._callbacks = new CallbackList()
|
||||
}
|
||||
this._callbacks.add(listener, thisArgs)
|
||||
|
||||
let result: Unsubscribable
|
||||
result = {
|
||||
unsubscribe: () => {
|
||||
this._callbacks!.remove(listener, thisArgs)
|
||||
result.unsubscribe = Emitter._noop
|
||||
},
|
||||
}
|
||||
if (Array.isArray(Unsubscribables)) {
|
||||
Unsubscribables.push(result)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
return this._event
|
||||
}
|
||||
|
||||
/**
|
||||
* To be kept private to fire an event to
|
||||
* subscribers
|
||||
*/
|
||||
public fire(event: T): any {
|
||||
if (this._callbacks) {
|
||||
this._callbacks.invoke.call(this._callbacks, event)
|
||||
}
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
if (this._callbacks) {
|
||||
this._callbacks.unsubscribe()
|
||||
this._callbacks = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
import { LinkedMap, Touch } from './linkedMap'
|
||||
|
||||
describe('LinkedMap', () => {
|
||||
test('simple', () => {
|
||||
const map = new LinkedMap<string, string>()
|
||||
map.set('ak', 'av')
|
||||
map.set('bk', 'bv')
|
||||
expect(Array.from(map.keys())).toEqual(['ak', 'bk'])
|
||||
expect(Array.from(map.values())).toEqual(['av', 'bv'])
|
||||
})
|
||||
|
||||
test('touch first', () => {
|
||||
const map = new LinkedMap<string, string>()
|
||||
map.set('ak', 'av')
|
||||
map.set('ak', 'av', Touch.First)
|
||||
expect(Array.from(map.keys())).toEqual(['ak'])
|
||||
expect(Array.from(map.values())).toEqual(['av'])
|
||||
})
|
||||
|
||||
test('touch last', () => {
|
||||
const map = new LinkedMap<string, string>()
|
||||
map.set('ak', 'av')
|
||||
map.set('ak', 'av', Touch.Last)
|
||||
expect(Array.from(map.keys())).toEqual(['ak'])
|
||||
expect(Array.from(map.values())).toEqual(['av'])
|
||||
})
|
||||
|
||||
test('touch first 2', () => {
|
||||
const map = new LinkedMap<string, string>()
|
||||
map.set('ak', 'av')
|
||||
map.set('bk', 'bv')
|
||||
map.set('bk', 'bv', Touch.First)
|
||||
expect(Array.from(map.keys())).toEqual(['bk', 'ak'])
|
||||
expect(Array.from(map.values())).toEqual(['bv', 'av'])
|
||||
})
|
||||
|
||||
test('touch last 2', () => {
|
||||
const map = new LinkedMap<string, string>()
|
||||
map.set('ak', 'av')
|
||||
map.set('bk', 'bv')
|
||||
map.set('ak', 'av', Touch.Last)
|
||||
expect(Array.from(map.keys())).toEqual(['bk', 'ak'])
|
||||
expect(Array.from(map.values())).toEqual(['bv', 'av'])
|
||||
})
|
||||
|
||||
test('touch first from middle', () => {
|
||||
const map = new LinkedMap<string, string>()
|
||||
map.set('ak', 'av')
|
||||
map.set('bk', 'bv')
|
||||
map.set('ck', 'cv')
|
||||
map.set('bk', 'bv', Touch.First)
|
||||
expect(Array.from(map.keys())).toEqual(['bk', 'ak', 'ck'])
|
||||
expect(Array.from(map.values())).toEqual(['bv', 'av', 'cv'])
|
||||
})
|
||||
|
||||
test('touch last from middle', () => {
|
||||
const map = new LinkedMap<string, string>()
|
||||
map.set('ak', 'av')
|
||||
map.set('bk', 'bv')
|
||||
map.set('ck', 'cv')
|
||||
map.set('bk', 'bv', Touch.Last)
|
||||
expect(Array.from(map.keys())).toEqual(['ak', 'ck', 'bk'])
|
||||
expect(Array.from(map.values())).toEqual(['av', 'cv', 'bv'])
|
||||
})
|
||||
})
|
||||
@ -1,272 +0,0 @@
|
||||
interface Item<K, V> {
|
||||
previous: Item<K, V> | undefined
|
||||
next: Item<K, V> | undefined
|
||||
key: K
|
||||
value: V
|
||||
}
|
||||
|
||||
export namespace Touch {
|
||||
export const None: 0 = 0
|
||||
export const First: 1 = 1
|
||||
export const Last: 2 = 2
|
||||
}
|
||||
|
||||
export type Touch = 0 | 1 | 2
|
||||
|
||||
export class LinkedMap<K, V> {
|
||||
private _map: Map<K, Item<K, V>>
|
||||
private _head: Item<K, V> | undefined
|
||||
private _tail: Item<K, V> | undefined
|
||||
private _size: number
|
||||
|
||||
constructor() {
|
||||
this._map = new Map<K, Item<K, V>>()
|
||||
this._head = undefined
|
||||
this._tail = undefined
|
||||
this._size = 0
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._map.clear()
|
||||
this._head = undefined
|
||||
this._tail = undefined
|
||||
this._size = 0
|
||||
}
|
||||
|
||||
public isEmpty(): boolean {
|
||||
return !this._head && !this._tail
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this._size
|
||||
}
|
||||
|
||||
public has(key: K): boolean {
|
||||
return this._map.has(key)
|
||||
}
|
||||
|
||||
public get(key: K): V | undefined {
|
||||
const item = this._map.get(key)
|
||||
if (!item) {
|
||||
return undefined
|
||||
}
|
||||
return item.value
|
||||
}
|
||||
|
||||
public set(key: K, value: V, touch: Touch = Touch.None): void {
|
||||
let item = this._map.get(key)
|
||||
if (item) {
|
||||
item.value = value
|
||||
if (touch !== Touch.None) {
|
||||
this.touch(item, touch)
|
||||
}
|
||||
} else {
|
||||
item = { key, value, next: undefined, previous: undefined }
|
||||
switch (touch) {
|
||||
case Touch.None:
|
||||
this.addItemLast(item)
|
||||
break
|
||||
case Touch.First:
|
||||
this.addItemFirst(item)
|
||||
break
|
||||
case Touch.Last:
|
||||
this.addItemLast(item)
|
||||
break
|
||||
default:
|
||||
this.addItemLast(item)
|
||||
break
|
||||
}
|
||||
this._map.set(key, item)
|
||||
this._size++
|
||||
}
|
||||
}
|
||||
|
||||
public delete(key: K): boolean {
|
||||
const item = this._map.get(key)
|
||||
if (!item) {
|
||||
return false
|
||||
}
|
||||
this._map.delete(key)
|
||||
this.removeItem(item)
|
||||
this._size--
|
||||
return true
|
||||
}
|
||||
|
||||
public shift(): V | undefined {
|
||||
if (!this._head && !this._tail) {
|
||||
return undefined
|
||||
}
|
||||
if (!this._head || !this._tail) {
|
||||
throw new Error('Invalid list')
|
||||
}
|
||||
const item = this._head
|
||||
this._map.delete(item.key)
|
||||
this.removeItem(item)
|
||||
this._size--
|
||||
return item.value
|
||||
}
|
||||
|
||||
public forEach(callbackfn: (value: V, key: K, map: LinkedMap<K, V>) => void, thisArg?: any): void {
|
||||
let current = this._head
|
||||
while (current) {
|
||||
if (thisArg) {
|
||||
callbackfn.bind(thisArg)(current.value, current.key, this)
|
||||
} else {
|
||||
callbackfn(current.value, current.key, this)
|
||||
}
|
||||
current = current.next
|
||||
}
|
||||
}
|
||||
|
||||
public forEachReverse(callbackfn: (value: V, key: K, map: LinkedMap<K, V>) => void, thisArg?: any): void {
|
||||
let current = this._tail
|
||||
while (current) {
|
||||
if (thisArg) {
|
||||
callbackfn.bind(thisArg)(current.value, current.key, this)
|
||||
} else {
|
||||
callbackfn(current.value, current.key, this)
|
||||
}
|
||||
current = current.previous
|
||||
}
|
||||
}
|
||||
|
||||
public keys(): IterableIterator<K> {
|
||||
let current = this._head
|
||||
const iterator: IterableIterator<K> = {
|
||||
[Symbol.iterator](): IterableIterator<K> {
|
||||
return iterator
|
||||
},
|
||||
next(): IteratorResult<K> {
|
||||
if (current) {
|
||||
const result = { value: current.key, done: false }
|
||||
current = current.next
|
||||
return result
|
||||
}
|
||||
return { done: true } as IteratorResult<K>
|
||||
},
|
||||
}
|
||||
return iterator
|
||||
}
|
||||
|
||||
public values(): IterableIterator<V> {
|
||||
let current = this._head
|
||||
const iterator: IterableIterator<V> = {
|
||||
[Symbol.iterator](): IterableIterator<V> {
|
||||
return iterator
|
||||
},
|
||||
next(): IteratorResult<V> {
|
||||
if (current) {
|
||||
const result = { value: current.value, done: false }
|
||||
current = current.next
|
||||
return result
|
||||
}
|
||||
return { done: true } as IteratorResult<V>
|
||||
},
|
||||
}
|
||||
return iterator
|
||||
}
|
||||
|
||||
private addItemFirst(item: Item<K, V>): void {
|
||||
// First time Insert
|
||||
if (!this._head && !this._tail) {
|
||||
this._tail = item
|
||||
} else if (!this._head) {
|
||||
throw new Error('Invalid list')
|
||||
} else {
|
||||
item.next = this._head
|
||||
this._head.previous = item
|
||||
}
|
||||
this._head = item
|
||||
}
|
||||
|
||||
private addItemLast(item: Item<K, V>): void {
|
||||
// First time Insert
|
||||
if (!this._head && !this._tail) {
|
||||
this._head = item
|
||||
} else if (!this._tail) {
|
||||
throw new Error('Invalid list')
|
||||
} else {
|
||||
item.previous = this._tail
|
||||
this._tail.next = item
|
||||
}
|
||||
this._tail = item
|
||||
}
|
||||
|
||||
private removeItem(item: Item<K, V>): void {
|
||||
if (item === this._head && item === this._tail) {
|
||||
this._head = undefined
|
||||
this._tail = undefined
|
||||
} else if (item === this._head) {
|
||||
this._head = item.next
|
||||
} else if (item === this._tail) {
|
||||
this._tail = item.previous
|
||||
} else {
|
||||
const next = item.next
|
||||
const previous = item.previous
|
||||
if (!next || !previous) {
|
||||
throw new Error('Invalid list')
|
||||
}
|
||||
next.previous = previous
|
||||
previous.next = next
|
||||
}
|
||||
}
|
||||
|
||||
private touch(item: Item<K, V>, touch: Touch): void {
|
||||
if (!this._head || !this._tail) {
|
||||
throw new Error('Invalid list')
|
||||
}
|
||||
if (touch !== Touch.First && touch !== Touch.Last) {
|
||||
return
|
||||
}
|
||||
|
||||
if (touch === Touch.First) {
|
||||
if (item === this._head) {
|
||||
return
|
||||
}
|
||||
|
||||
const next = item.next
|
||||
const previous = item.previous
|
||||
|
||||
// Unlink the item
|
||||
if (item === this._tail) {
|
||||
// previous must be defined since item was not head but is tail
|
||||
// So there are more than on item in the map
|
||||
previous!.next = undefined
|
||||
this._tail = previous
|
||||
} else {
|
||||
// Both next and previous are not undefined since item was neither head nor tail.
|
||||
next!.previous = previous
|
||||
previous!.next = next
|
||||
}
|
||||
|
||||
// Insert the node at head
|
||||
item.previous = undefined
|
||||
item.next = this._head
|
||||
this._head.previous = item
|
||||
this._head = item
|
||||
} else if (touch === Touch.Last) {
|
||||
if (item === this._tail) {
|
||||
return
|
||||
}
|
||||
|
||||
const next = item.next
|
||||
const previous = item.previous
|
||||
|
||||
// Unlink the item.
|
||||
if (item === this._head) {
|
||||
// next must be defined since item was not tail but is head
|
||||
// So there are more than on item in the map
|
||||
next!.previous = undefined
|
||||
this._head = next
|
||||
} else {
|
||||
// Both next and previous are not undefined since item was neither head nor tail.
|
||||
next!.previous = previous
|
||||
previous!.next = next
|
||||
}
|
||||
item.next = undefined
|
||||
item.previous = this._tail
|
||||
this._tail.next = item
|
||||
this._tail = item
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,167 +0,0 @@
|
||||
/**
|
||||
* A language server message
|
||||
*/
|
||||
export interface Message {
|
||||
jsonrpc: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The request ID.
|
||||
*/
|
||||
export type RequestID = number | string
|
||||
|
||||
/**
|
||||
* Request message
|
||||
*/
|
||||
export interface RequestMessage extends Message {
|
||||
/**
|
||||
* The request ID.
|
||||
*/
|
||||
id: RequestID
|
||||
|
||||
/**
|
||||
* The method to be invoked.
|
||||
*/
|
||||
method: string
|
||||
|
||||
/**
|
||||
* The method's params.
|
||||
*/
|
||||
params?: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Predefined error codes.
|
||||
*/
|
||||
export namespace ErrorCodes {
|
||||
// Defined by JSON-RPC 2.0.
|
||||
export const ParseError = -32700
|
||||
export const InvalidRequest = -32600
|
||||
export const MethodNotFound = -32601
|
||||
export const InvalidParams = -32602
|
||||
export const InternalError = -32603
|
||||
export const serverErrorStart = -32099
|
||||
export const serverErrorEnd = -32000
|
||||
export const ServerNotInitialized = -32002
|
||||
export const UnknownErrorCode = -32001
|
||||
|
||||
// Defined by this library.
|
||||
export const MessageWriteError = 1
|
||||
export const MessageReadError = 2
|
||||
export const RequestAborted = -32800
|
||||
}
|
||||
|
||||
interface ResponseErrorLiteral<D> {
|
||||
/**
|
||||
* A number indicating the error type that occured.
|
||||
*/
|
||||
code: number
|
||||
|
||||
/**
|
||||
* A string providing a short decription of the error.
|
||||
*/
|
||||
message: string
|
||||
|
||||
/**
|
||||
* A Primitive or Structured value that contains additional
|
||||
* information about the error. Can be omitted.
|
||||
*/
|
||||
data?: D
|
||||
}
|
||||
|
||||
/**
|
||||
* An error object return in a response in case a request
|
||||
* has failed.
|
||||
*/
|
||||
export class ResponseError<D> extends Error {
|
||||
public readonly code: number
|
||||
public readonly data: D | undefined
|
||||
|
||||
constructor(code: number, message: string, data?: D) {
|
||||
super(message)
|
||||
this.code = typeof code === 'number' ? code : ErrorCodes.UnknownErrorCode
|
||||
this.data = data
|
||||
Object.setPrototypeOf(this, ResponseError.prototype)
|
||||
}
|
||||
|
||||
public toJSON(): ResponseErrorLiteral<D> {
|
||||
return {
|
||||
code: this.code,
|
||||
message: this.message,
|
||||
data: this.data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A response message.
|
||||
*/
|
||||
export interface ResponseMessage extends Message {
|
||||
/**
|
||||
* The request id.
|
||||
*/
|
||||
id: number | string | null
|
||||
|
||||
/**
|
||||
* The result of a request. This can be omitted in
|
||||
* the case of an error.
|
||||
*/
|
||||
result?: any
|
||||
|
||||
/**
|
||||
* The error object in case a request fails.
|
||||
*/
|
||||
error?: ResponseErrorLiteral<any>
|
||||
|
||||
/**
|
||||
* Whether the request is completed (no more values will be emitted).
|
||||
*/
|
||||
complete?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Notification Message
|
||||
*/
|
||||
export interface NotificationMessage extends Message {
|
||||
/**
|
||||
* The method to be invoked.
|
||||
*/
|
||||
method: string
|
||||
|
||||
/**
|
||||
* The notification's params.
|
||||
*/
|
||||
params?: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the given message is a request message
|
||||
*/
|
||||
export function isRequestMessage(message: Message | undefined): message is RequestMessage {
|
||||
const candidate = message as RequestMessage
|
||||
return (
|
||||
candidate &&
|
||||
typeof candidate.method === 'string' &&
|
||||
(typeof candidate.id === 'string' || typeof candidate.id === 'number')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the given message is a notification message
|
||||
*/
|
||||
export function isNotificationMessage(message: Message | undefined): message is NotificationMessage {
|
||||
const candidate = message as NotificationMessage
|
||||
return candidate && typeof candidate.method === 'string' && (message as any).id === void 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the given message is a response message
|
||||
*/
|
||||
export function isResponseMessage(message: Message | undefined): message is ResponseMessage {
|
||||
const candidate = message as ResponseMessage
|
||||
return (
|
||||
candidate &&
|
||||
(candidate.result !== void 0 || !!candidate.error || !!candidate.complete) &&
|
||||
(typeof candidate.id === 'string' || typeof candidate.id === 'number' || candidate.id === null)
|
||||
)
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
import { MessageTransports } from './connection'
|
||||
import { Message } from './messages'
|
||||
import { AbstractMessageReader, AbstractMessageWriter, DataCallback, MessageReader, MessageWriter } from './transport'
|
||||
|
||||
/**
|
||||
* Creates a pair of message transports that are connected to each other. One can be used as the server and the
|
||||
* other as the client.
|
||||
*/
|
||||
export function createMessageTransports(): [MessageTransports, MessageTransports] {
|
||||
const { reader: reader1, writer: writer1 } = createMessagePipe()
|
||||
const { reader: reader2, writer: writer2 } = createMessagePipe()
|
||||
return [{ reader: reader1, writer: writer2 }, { reader: reader2, writer: writer1 }]
|
||||
}
|
||||
|
||||
/** Creates a single set of transports that are connected to each other. */
|
||||
export function createMessagePipe(): MessageTransports {
|
||||
let readerCallback: DataCallback | undefined
|
||||
const reader: MessageReader = new class extends AbstractMessageReader implements MessageReader {
|
||||
public listen(callback: DataCallback): void {
|
||||
readerCallback = callback
|
||||
}
|
||||
}()
|
||||
const writer: MessageWriter = new class extends AbstractMessageWriter implements MessageWriter {
|
||||
public write(msg: Message): void {
|
||||
if (readerCallback) {
|
||||
readerCallback(msg)
|
||||
} else {
|
||||
throw new Error('reader has no listener')
|
||||
}
|
||||
}
|
||||
}()
|
||||
return { reader, writer }
|
||||
}
|
||||
@ -1,124 +0,0 @@
|
||||
import { NotificationMessage, RequestMessage, ResponseMessage } from './messages'
|
||||
|
||||
// Copied from vscode-jsonrpc to avoid adding extraneous dependencies.
|
||||
|
||||
/** Records messages sent and received on a JSON-RPC 2.0 connection. */
|
||||
export interface Tracer {
|
||||
log(message: string, details?: string): void
|
||||
requestSent(message: RequestMessage): void
|
||||
requestReceived(message: RequestMessage): void
|
||||
notificationSent(message: NotificationMessage): void
|
||||
notificationReceived(message: NotificationMessage): void
|
||||
responseSent(message: ResponseMessage, request: RequestMessage, startTime: number): void
|
||||
responseAborted(message: ResponseMessage, request: RequestMessage, abortMessage: NotificationMessage): void
|
||||
responseReceived(message: ResponseMessage, request: RequestMessage | string, startTime: number): void
|
||||
unknownResponseReceived(message: ResponseMessage): void
|
||||
}
|
||||
|
||||
/** A tracer that implements the Tracer interface with noop methods. */
|
||||
export const noopTracer: Tracer = {
|
||||
log: () => void 0,
|
||||
requestSent: () => void 0,
|
||||
requestReceived: () => void 0,
|
||||
notificationSent: () => void 0,
|
||||
notificationReceived: () => void 0,
|
||||
responseSent: () => void 0,
|
||||
responseAborted: () => void 0,
|
||||
responseReceived: () => void 0,
|
||||
unknownResponseReceived: () => void 0,
|
||||
}
|
||||
|
||||
/** A tracer that implements the Tracer interface with console API calls, intended for a web browser. */
|
||||
export class BrowserConsoleTracer implements Tracer {
|
||||
public constructor(private name: string) {}
|
||||
|
||||
private prefix(level: 'info' | 'error', label: string, title: string): string[] {
|
||||
let color: string
|
||||
let backgroundColor: string
|
||||
if (level === 'info') {
|
||||
color = '#000'
|
||||
backgroundColor = '#eee'
|
||||
} else {
|
||||
color = 'white'
|
||||
backgroundColor = 'red'
|
||||
}
|
||||
return [
|
||||
'%c%s%c %s%c%s%c',
|
||||
`font-weight:bold;background-color:#d8f7ff;color:black`,
|
||||
this.name,
|
||||
'',
|
||||
label,
|
||||
`background-color:${backgroundColor};color:${color};font-weight:bold`,
|
||||
title,
|
||||
'',
|
||||
]
|
||||
}
|
||||
|
||||
public log(message: string, details?: string): void {
|
||||
if (details) {
|
||||
;(console.groupCollapsed as any)(...this.prefix('info', 'log', ''), message)
|
||||
console.log(details)
|
||||
console.groupEnd()
|
||||
} else {
|
||||
console.log(...this.prefix('info', 'log', ''), message)
|
||||
}
|
||||
}
|
||||
|
||||
public requestSent(message: RequestMessage): void {
|
||||
console.log(...this.prefix('info', `◀◀ sent request #${message.id}: `, message.method), message.params)
|
||||
}
|
||||
|
||||
public requestReceived(message: RequestMessage): void {
|
||||
console.log(...this.prefix('info', `▶▶ recv request #${message.id}: `, message.method), message.params)
|
||||
}
|
||||
|
||||
public notificationSent(message: NotificationMessage): void {
|
||||
console.log(...this.prefix('info', `◀◀ sent notif: `, message.method), message.params)
|
||||
}
|
||||
|
||||
public notificationReceived(message: NotificationMessage): void {
|
||||
console.log(...this.prefix('info', `▶▶ recv notif: `, message.method), message.params)
|
||||
}
|
||||
|
||||
public responseSent(message: ResponseMessage, request: RequestMessage, startTime: number): void {
|
||||
const prefix = this.prefix(
|
||||
message.error ? 'error' : 'info',
|
||||
`◀▶ sent response #${message.id}: `,
|
||||
typeof request === 'string' ? request : request.method
|
||||
)
|
||||
;(console.groupCollapsed as any)(...prefix)
|
||||
console.log('Response:', message)
|
||||
console.log('Request:', request)
|
||||
console.log('Duration: %d msec', Date.now() - startTime)
|
||||
console.groupEnd()
|
||||
}
|
||||
|
||||
public responseAborted(
|
||||
_message: ResponseMessage,
|
||||
request: RequestMessage,
|
||||
_abortMessage: NotificationMessage
|
||||
): void {
|
||||
console.log(...this.prefix('info', '× abort: ', request.method))
|
||||
}
|
||||
|
||||
public responseReceived(message: ResponseMessage, request: RequestMessage | string, startTime: number): void {
|
||||
const prefix = this.prefix(
|
||||
message.error ? 'error' : 'info',
|
||||
`◀▶ recv response #${message.id}: `,
|
||||
typeof request === 'string' ? request : request.method
|
||||
)
|
||||
if (typeof request === 'string') {
|
||||
console.log(...prefix, message.error || message.result)
|
||||
} else {
|
||||
;(console.groupCollapsed as any)(...prefix)
|
||||
console.log('Response:', message)
|
||||
console.log('Request:', request)
|
||||
console.log('Duration: %d msec', Date.now() - startTime)
|
||||
console.groupEnd()
|
||||
}
|
||||
}
|
||||
|
||||
public unknownResponseReceived(message: ResponseMessage): void {
|
||||
console.log(...this.prefix('error', 'UNKNOWN', ''), message)
|
||||
}
|
||||
}
|
||||
@ -1,95 +0,0 @@
|
||||
import { Emitter, Event } from './events'
|
||||
import { Message } from './messages'
|
||||
|
||||
// Copied from vscode-jsonrpc to avoid adding extraneous dependencies.
|
||||
|
||||
export type DataCallback = (data: Message) => void
|
||||
|
||||
export interface MessageReader {
|
||||
readonly onError: Event<Error>
|
||||
readonly onClose: Event<void>
|
||||
listen(callback: DataCallback): void
|
||||
unsubscribe(): void
|
||||
}
|
||||
|
||||
export abstract class AbstractMessageReader {
|
||||
private errorEmitter: Emitter<Error>
|
||||
private closeEmitter: Emitter<void>
|
||||
|
||||
constructor() {
|
||||
this.errorEmitter = new Emitter<Error>()
|
||||
this.closeEmitter = new Emitter<void>()
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
this.errorEmitter.unsubscribe()
|
||||
this.closeEmitter.unsubscribe()
|
||||
}
|
||||
|
||||
public get onError(): Event<Error> {
|
||||
return this.errorEmitter.event
|
||||
}
|
||||
|
||||
protected fireError(error: any): void {
|
||||
this.errorEmitter.fire(this.asError(error))
|
||||
}
|
||||
|
||||
public get onClose(): Event<void> {
|
||||
return this.closeEmitter.event
|
||||
}
|
||||
|
||||
protected fireClose(): void {
|
||||
this.closeEmitter.fire(undefined)
|
||||
}
|
||||
|
||||
private asError(error: any): Error {
|
||||
if (error instanceof Error) {
|
||||
return error
|
||||
}
|
||||
return new Error(
|
||||
`Reader received error. Reason: ${typeof error.message === 'string' ? error.message : 'unknown'}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export interface MessageWriter {
|
||||
readonly onError: Event<[Error, Message | undefined, number | undefined]>
|
||||
readonly onClose: Event<void>
|
||||
write(msg: Message): void
|
||||
unsubscribe(): void
|
||||
}
|
||||
|
||||
export abstract class AbstractMessageWriter {
|
||||
private errorEmitter = new Emitter<[Error, Message | undefined, number | undefined]>()
|
||||
private closeEmitter = new Emitter<void>()
|
||||
|
||||
public unsubscribe(): void {
|
||||
this.errorEmitter.unsubscribe()
|
||||
this.closeEmitter.unsubscribe()
|
||||
}
|
||||
|
||||
public get onError(): Event<[Error, Message | undefined, number | undefined]> {
|
||||
return this.errorEmitter.event
|
||||
}
|
||||
|
||||
protected fireError(error: any, message?: Message, count?: number): void {
|
||||
this.errorEmitter.fire([this.asError(error), message, count])
|
||||
}
|
||||
|
||||
public get onClose(): Event<void> {
|
||||
return this.closeEmitter.event
|
||||
}
|
||||
|
||||
protected fireClose(): void {
|
||||
this.closeEmitter.fire(undefined)
|
||||
}
|
||||
|
||||
private asError(error: any): Error {
|
||||
if (error instanceof Error) {
|
||||
return error
|
||||
}
|
||||
return new Error(
|
||||
`Writer received error. Reason: ${typeof error.message === 'string' ? error.message : 'unknown'}`
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,130 +0,0 @@
|
||||
import { MessageTransports } from '../connection'
|
||||
import { Message } from '../messages'
|
||||
import { AbstractMessageReader, AbstractMessageWriter, DataCallback, MessageReader, MessageWriter } from '../transport'
|
||||
|
||||
// TODO: use transferable objects in postMessage for perf
|
||||
|
||||
// Copied subset of Worker from the TypeScript "dom"/"webworker" core libraries to avoid needing to add those libs
|
||||
// to tsconfig.json.
|
||||
interface MessageEvent {
|
||||
data: any
|
||||
}
|
||||
interface WorkerEventMap {
|
||||
error: any
|
||||
message: MessageEvent
|
||||
}
|
||||
interface Worker {
|
||||
postMessage(message: any): void
|
||||
addEventListener<K extends keyof WorkerEventMap>(
|
||||
type: K,
|
||||
listener: (this: Worker, ev: WorkerEventMap[K]) => any
|
||||
): void
|
||||
close?(): void
|
||||
terminate?(): void
|
||||
}
|
||||
|
||||
class WebWorkerMessageReader extends AbstractMessageReader implements MessageReader {
|
||||
private pending: Message[] = []
|
||||
private callback: DataCallback | null = null
|
||||
|
||||
constructor(private worker: Worker) {
|
||||
super()
|
||||
|
||||
worker.addEventListener('message', (e: MessageEvent) => {
|
||||
try {
|
||||
this.processMessage(e)
|
||||
} catch (err) {
|
||||
this.fireError(err)
|
||||
}
|
||||
})
|
||||
worker.addEventListener('error', (err: ErrorEvent) => {
|
||||
this.fireError(err)
|
||||
if (err.cancelable) {
|
||||
err.preventDefault()
|
||||
} else {
|
||||
terminateWorker(worker)
|
||||
this.fireClose()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private processMessage(e: MessageEvent): void {
|
||||
const message: Message = e.data
|
||||
if (this.callback) {
|
||||
this.callback(message)
|
||||
} else {
|
||||
this.pending.push(message)
|
||||
}
|
||||
}
|
||||
|
||||
public listen(callback: DataCallback): void {
|
||||
if (this.callback) {
|
||||
throw new Error('callback is already set')
|
||||
}
|
||||
this.callback = callback
|
||||
while (this.pending.length !== 0) {
|
||||
callback(this.pending.pop()!)
|
||||
}
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
super.unsubscribe()
|
||||
this.callback = null
|
||||
terminateWorker(this.worker)
|
||||
}
|
||||
}
|
||||
|
||||
class WebWorkerMessageWriter extends AbstractMessageWriter implements MessageWriter {
|
||||
private errorCount = 0
|
||||
|
||||
constructor(private worker: Worker) {
|
||||
super()
|
||||
}
|
||||
|
||||
public write(message: Message): void {
|
||||
try {
|
||||
this.worker.postMessage(message)
|
||||
} catch (error) {
|
||||
this.fireError(error, message, ++this.errorCount)
|
||||
}
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
super.unsubscribe()
|
||||
terminateWorker(this.worker)
|
||||
}
|
||||
}
|
||||
|
||||
function terminateWorker(worker: Worker): void {
|
||||
if (worker.terminate) {
|
||||
worker.terminate() // in window (worker parent) scope
|
||||
} else if (worker.close) {
|
||||
worker.close() // in worker scope
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates JSON-RPC2 message transports for the Web Worker message communication interface.
|
||||
*
|
||||
* @param worker The Worker to communicate with (e.g., created with `new Worker(...)`), or the global scope (i.e.,
|
||||
* `self`) if the current execution context is in a Worker. Defaults to the global scope.
|
||||
*/
|
||||
export function createWebWorkerMessageTransports(worker: Worker = globalWorkerScope()): MessageTransports {
|
||||
const reader = new WebWorkerMessageReader(worker)
|
||||
const writer = new WebWorkerMessageWriter(worker)
|
||||
reader.onError(err => console.error(err))
|
||||
writer.onError(err => console.error(err))
|
||||
return {
|
||||
reader,
|
||||
writer,
|
||||
}
|
||||
}
|
||||
|
||||
function globalWorkerScope(): Worker {
|
||||
const worker: Worker = global as any
|
||||
// tslint:disable-next-line no-unbound-method
|
||||
if (!worker.postMessage || 'document' in worker) {
|
||||
throw new Error('global scope is not a Worker')
|
||||
}
|
||||
return worker
|
||||
}
|
||||
@ -1,4 +1,23 @@
|
||||
import { Subscribable } from 'sourcegraph'
|
||||
import { ProxiedObject, ProxyValue } from '@sourcegraph/comlink'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { Subscribable, Unsubscribable } from 'sourcegraph'
|
||||
|
||||
/**
|
||||
* Creates a synchronous Subscription that will unsubscribe the given proxied Subscription asynchronously.
|
||||
*
|
||||
* @param subscriptionPromise A Promise for a Subscription proxied from the other thread
|
||||
*/
|
||||
export const syncSubscription = (
|
||||
subscriptionPromise: Promise<ProxiedObject<Unsubscribable & ProxyValue>>
|
||||
): Subscription =>
|
||||
// We cannot pass the proxy subscription directly to Rx because it is a Proxy that looks like a function
|
||||
new Subscription(() => {
|
||||
// tslint:disable-next-line: no-floating-promises
|
||||
subscriptionPromise.then(proxySubscription => {
|
||||
// tslint:disable-next-line: no-floating-promises
|
||||
proxySubscription.unsubscribe()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Runs f and returns a resolved promise with its value or a rejected promise with its exception,
|
||||
|
||||
@ -6,7 +6,6 @@ import { catchError, distinctUntilChanged, map, switchMap } from 'rxjs/operators
|
||||
import { ExecutableExtension } from '../api/client/services/extensionsService'
|
||||
import { Link } from '../components/Link'
|
||||
import { PopoverButton } from '../components/PopoverButton'
|
||||
import { Toggle } from '../components/Toggle'
|
||||
import { ExtensionsControllerProps } from '../extensions/controller'
|
||||
import { PlatformContextProps } from '../platform/context'
|
||||
import { asError, ErrorLike, isErrorLike } from '../util/errors'
|
||||
@ -19,8 +18,6 @@ interface State {
|
||||
/** The extension IDs of extensions that are active, an error, or undefined while loading. */
|
||||
extensionsOrError?: Pick<ExecutableExtension, 'id'>[] | ErrorLike
|
||||
|
||||
/** Whether to log traces of communication with extensions. */
|
||||
traceExtensionHostCommunication?: boolean
|
||||
sideloadedExtensionURL?: string | null
|
||||
}
|
||||
|
||||
@ -49,14 +46,6 @@ export class ExtensionStatus extends React.PureComponent<Props, State> {
|
||||
map(({ platformContext }) => platformContext),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
this.subscriptions.add(
|
||||
platformContext
|
||||
.pipe(
|
||||
switchMap(({ traceExtensionHostCommunication }) => traceExtensionHostCommunication),
|
||||
map(traceExtensionHostCommunication => ({ traceExtensionHostCommunication }))
|
||||
)
|
||||
.subscribe(stateUpdate => this.setState({ ...this.state, ...stateUpdate }))
|
||||
)
|
||||
|
||||
this.subscriptions.add(
|
||||
platformContext
|
||||
@ -101,17 +90,6 @@ export class ExtensionStatus extends React.PureComponent<Props, State> {
|
||||
<LoadingSpinner className="icon-inline" /> Loading extensions...
|
||||
</span>
|
||||
)}
|
||||
<div className="card-body border-top d-flex justify-content-start align-items-center">
|
||||
<label htmlFor="extension-status__trace" className="mr-2 mb-0">
|
||||
Log to devtools console{' '}
|
||||
</label>
|
||||
<Toggle
|
||||
id="extension-status__trace"
|
||||
onToggle={this.onToggleTrace}
|
||||
value={this.state.traceExtensionHostCommunication}
|
||||
title="Toggle extension trace logging to devtools console"
|
||||
/>
|
||||
</div>
|
||||
<div className="card-body border-top">
|
||||
<h6>Sideload extension</h6>
|
||||
{this.state.sideloadedExtensionURL ? (
|
||||
@ -149,10 +127,6 @@ export class ExtensionStatus extends React.PureComponent<Props, State> {
|
||||
)
|
||||
}
|
||||
|
||||
private onToggleTrace = () => {
|
||||
this.props.platformContext.traceExtensionHostCommunication.next(!this.state.traceExtensionHostCommunication)
|
||||
}
|
||||
|
||||
private setSideloadedExtensionURL = () => {
|
||||
const url = window.prompt(
|
||||
'Parcel dev server URL:',
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { combineLatest, from, Observable, Subject, Subscription, Unsubscribable } from 'rxjs'
|
||||
import { distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'
|
||||
import { from, Observable, Subject, Subscription, Unsubscribable } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { createExtensionHostClient } from '../api/client/client'
|
||||
import { Services } from '../api/client/services'
|
||||
import { ExecuteCommandParams } from '../api/client/services/command'
|
||||
@ -8,8 +8,6 @@ import { ExtensionsService } from '../api/client/services/extensionsService'
|
||||
import { MessageType } from '../api/client/services/notifications'
|
||||
import { InitData } from '../api/extension/extensionHost'
|
||||
import { Contributions } from '../api/protocol'
|
||||
import { createConnection } from '../api/protocol/jsonrpc2/connection'
|
||||
import { BrowserConsoleTracer } from '../api/protocol/jsonrpc2/trace'
|
||||
import { registerBuiltinClientCommands } from '../commands/commands'
|
||||
import { Notification } from '../notifications/notification'
|
||||
import { PlatformContext } from '../platform/context'
|
||||
@ -67,30 +65,12 @@ export function createController(context: PlatformContext): Controller {
|
||||
const subscriptions = new Subscription()
|
||||
|
||||
const services = new Services(context)
|
||||
const extensionHostConnection = combineLatest(
|
||||
context.createExtensionHost().pipe(
|
||||
switchMap(async messageTransports => {
|
||||
const connection = createConnection(messageTransports)
|
||||
connection.listen()
|
||||
|
||||
const initData: InitData = {
|
||||
sourcegraphURL: context.sourcegraphURL,
|
||||
clientApplication: context.clientApplication,
|
||||
}
|
||||
await connection.sendRequest('initialize', [initData])
|
||||
return connection
|
||||
}),
|
||||
share()
|
||||
),
|
||||
context.traceExtensionHostCommunication
|
||||
).pipe(
|
||||
tap(([connection, trace]) => {
|
||||
connection.trace(trace ? new BrowserConsoleTracer('') : null)
|
||||
}),
|
||||
map(([connection]) => connection),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
const client = createExtensionHostClient(services, extensionHostConnection)
|
||||
const extensionHostEndpoint = context.createExtensionHost()
|
||||
const initData: InitData = {
|
||||
sourcegraphURL: context.sourcegraphURL,
|
||||
clientApplication: context.clientApplication,
|
||||
}
|
||||
const client = createExtensionHostClient(services, extensionHostEndpoint, initData)
|
||||
subscriptions.add(client)
|
||||
|
||||
const notifications = new Subject<Notification>()
|
||||
|
||||
11
shared/src/globals.d.ts
vendored
Normal file
11
shared/src/globals.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* For Web Worker entrypoints using Webpack's worker-loader.
|
||||
*
|
||||
* See https://github.com/webpack-contrib/worker-loader#integrating-with-typescript.
|
||||
*/
|
||||
declare module 'worker-loader?inline!*' {
|
||||
class WebpackWorker extends Worker {
|
||||
constructor()
|
||||
}
|
||||
export default WebpackWorker
|
||||
}
|
||||
@ -1,11 +1,21 @@
|
||||
import { Endpoint, isEndpoint } from '@sourcegraph/comlink'
|
||||
import { NextObserver, Observable, Subscribable } from 'rxjs'
|
||||
import { SettingsEdit } from '../api/client/services/settings'
|
||||
import { MessageTransports } from '../api/protocol/jsonrpc2/connection'
|
||||
import { GraphQLResult } from '../graphql/graphql'
|
||||
import * as GQL from '../graphql/schema'
|
||||
import { Settings, SettingsCascadeOrError } from '../settings/settings'
|
||||
import { FileSpec, PositionSpec, RepoSpec, RevSpec, ViewStateSpec } from '../util/url'
|
||||
|
||||
export interface EndpointPair {
|
||||
/** The endpoint to proxy the API of the other thread from */
|
||||
proxy: Endpoint & Pick<MessagePort, 'start'>
|
||||
|
||||
/** The endpoint to expose the API of this thread to */
|
||||
expose: Endpoint & Pick<MessagePort, 'start'>
|
||||
}
|
||||
export const isEndpointPair = (val: any): val is EndpointPair =>
|
||||
typeof val === 'object' && val !== null && isEndpoint(val.proxy) && isEndpoint(val.expose)
|
||||
|
||||
/**
|
||||
* Platform-specific data and methods shared by multiple Sourcegraph components.
|
||||
*
|
||||
@ -68,7 +78,7 @@ export interface PlatformContext {
|
||||
* @returns An observable that emits at most once with the message transports for communicating
|
||||
* with the execution context (using, e.g., postMessage/onmessage) when it is ready.
|
||||
*/
|
||||
createExtensionHost(): Observable<MessageTransports>
|
||||
createExtensionHost(): Observable<EndpointPair>
|
||||
|
||||
/**
|
||||
* Returns the script URL suitable for passing to importScripts for an extension's bundle.
|
||||
@ -116,11 +126,6 @@ export interface PlatformContext {
|
||||
*/
|
||||
clientApplication: 'sourcegraph' | 'other'
|
||||
|
||||
/**
|
||||
* Whether to log all messages sent between the client and the extension host.
|
||||
*/
|
||||
traceExtensionHostCommunication: Subscribable<boolean> & NextObserver<boolean>
|
||||
|
||||
/**
|
||||
* The URL to the Parcel dev server for a single extension.
|
||||
* Used for extension development purposes, to run an extension that isn't on the registry.
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import 'symbol-observable'
|
||||
|
||||
// This gets automatically expanded into
|
||||
// imports that only pick what we need
|
||||
import '@babel/polyfill'
|
||||
@ -5,4 +7,5 @@ import '@babel/polyfill'
|
||||
// Polyfill URL because Chrome and Firefox are not spec-compliant
|
||||
// Hostnames of URIs with custom schemes (e.g. git) are not parsed out
|
||||
import { URL, URLSearchParams } from 'whatwg-url'
|
||||
Object.assign(window, { URL, URLSearchParams })
|
||||
Object.assign(URL, self.URL) // keep static methods like URL.createObjectURL()
|
||||
Object.assign(self, { URL, URLSearchParams })
|
||||
@ -3,7 +3,7 @@
|
||||
// Order is important here
|
||||
// Don't remove the empty lines between these imports
|
||||
|
||||
import '../polyfills'
|
||||
import '../../../shared/src/polyfills'
|
||||
|
||||
import '../sentry'
|
||||
|
||||
|
||||
2
web/src/globals.d.ts
vendored
2
web/src/globals.d.ts
vendored
@ -119,7 +119,7 @@ interface SourcegraphContext {
|
||||
*
|
||||
* See https://github.com/webpack-contrib/worker-loader#integrating-with-typescript.
|
||||
*/
|
||||
declare module 'worker-loader!*' {
|
||||
declare module 'worker-loader?inline!*' {
|
||||
class WebpackWorker extends Worker {
|
||||
constructor()
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
// Order is important here
|
||||
// Don't remove the empty lines between these imports
|
||||
|
||||
import './polyfills'
|
||||
import '../../shared/src/polyfills'
|
||||
|
||||
import './sentry'
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { concat, Observable, ReplaySubject } from 'rxjs'
|
||||
import { concat, ReplaySubject } from 'rxjs'
|
||||
import { map, publishReplay, refCount } from 'rxjs/operators'
|
||||
import ExtensionHostWorker from 'worker-loader!../../../shared/src/api/extension/main.worker.ts'
|
||||
import { createWebWorkerMessageTransports } from '../../../shared/src/api/protocol/jsonrpc2/transports/webWorker'
|
||||
import { createExtensionHost } from '../../../shared/src/api/extension/worker'
|
||||
import { gql } from '../../../shared/src/graphql/graphql'
|
||||
import * as GQL from '../../../shared/src/graphql/schema'
|
||||
import { PlatformContext } from '../../../shared/src/platform/context'
|
||||
@ -61,19 +60,11 @@ export function createPlatformContext(): PlatformContext {
|
||||
variables
|
||||
),
|
||||
forceUpdateTooltip: () => Tooltip.forceUpdate(),
|
||||
createExtensionHost: () => {
|
||||
const worker = new ExtensionHostWorker()
|
||||
const messageTransports = createWebWorkerMessageTransports(worker)
|
||||
return new Observable(sub => {
|
||||
sub.next(messageTransports)
|
||||
return () => worker.terminate()
|
||||
})
|
||||
},
|
||||
createExtensionHost: () => createExtensionHost({ wrapEndpoints: false }),
|
||||
urlToFile: toPrettyBlobURL,
|
||||
getScriptURLForExtension: bundleURL => bundleURL,
|
||||
sourcegraphURL: window.context.externalURL,
|
||||
clientApplication: 'sourcegraph',
|
||||
traceExtensionHostCommunication: new LocalStorageSubject<boolean>('traceExtensionHostCommunication', false),
|
||||
sideloadedExtensionURL: new LocalStorageSubject<string | null>('sideloadedExtensionURL', null),
|
||||
}
|
||||
return context
|
||||
|
||||
24
yarn.lock
24
yarn.lock
@ -1488,6 +1488,11 @@
|
||||
rxjs "^6.3.3"
|
||||
ts-key-enum "^2.0.0"
|
||||
|
||||
"@sourcegraph/comlink@^3.1.1-fork.3":
|
||||
version "3.1.1-fork.3"
|
||||
resolved "https://registry.npmjs.org/@sourcegraph/comlink/-/comlink-3.1.1-fork.3.tgz#1bd76bc74ca8a9b55f1dbf508c703053ee66c961"
|
||||
integrity sha512-+vfSCJTS/GU391Hhdh2/s+wy5ok4hi6X+kizRQqQZwZI03Q1rR6SVLNFsbg4dhbX624mqr1UCRhwNnZdWHAR+A==
|
||||
|
||||
"@sourcegraph/event-positions@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/@sourcegraph/event-positions/-/event-positions-1.0.1.tgz#aa83c38170275db4e696a6d5fc0f4d1e733713be"
|
||||
@ -1505,9 +1510,8 @@
|
||||
sourcegraph "*"
|
||||
|
||||
"@sourcegraph/extension-api-types@link:packages/@sourcegraph/extension-api-types":
|
||||
version "1.1.0"
|
||||
dependencies:
|
||||
sourcegraph "*"
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@sourcegraph/prettierrc@^2.2.0":
|
||||
version "2.2.0"
|
||||
@ -10178,6 +10182,11 @@ mermaid@^8.0.0:
|
||||
moment "^2.23.0"
|
||||
scope-css "^1.2.1"
|
||||
|
||||
message-port-polyfill@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/message-port-polyfill/-/message-port-polyfill-0.1.0.tgz#0d6c9201e5e15301f36930c4b7fe0a56c17bc7a7"
|
||||
integrity sha512-CKP0YTyLUnGZU+1SCqIm/QFE/oqo0oA4mLTb6BWSx5/YPlvlut32dpmYSr8u55xqAzy89Tc9X7bMdSwXtZJ2Ew==
|
||||
|
||||
methods@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
||||
@ -13780,12 +13789,13 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
sourcegraph@*:
|
||||
version "19.4.0"
|
||||
resolved "https://registry.npmjs.org/sourcegraph/-/sourcegraph-19.4.0.tgz#5e02146ef06a5a3b0e37d1f09375bbfa011ddee8"
|
||||
integrity sha512-fyx0nFtySTdn+Y1JR7LPdl2lj9vHCbSgoLjzMEW5iuBjVuVHXdVldq3kjBYoI98MI6L53qATT993sDVrq4Jqwg==
|
||||
version "21.0.0"
|
||||
resolved "https://registry.npmjs.org/sourcegraph/-/sourcegraph-21.0.0.tgz#b84ef37a0655574759d5f4a7ee5c1da388111580"
|
||||
integrity sha512-PcsvCepo3vORtzKgcjeaXMQcL+BQj2OO/Lkq7S0/k5bexefn6Wf5WrFD3SXJFqPVOWJM1hPG1tw1ME9Z5Gne3Q==
|
||||
|
||||
"sourcegraph@link:packages/sourcegraph-extension-api":
|
||||
version "22.0.0"
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
space-separated-tokens@^1.0.0:
|
||||
version "1.1.2"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user