mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-04-01 09:30:31 -04:00
Merge from master
This commit is contained in:
43
src/vs/base/parts/contextmenu/common/contextmenu.ts
Normal file
43
src/vs/base/parts/contextmenu/common/contextmenu.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface ICommonContextMenuItem {
|
||||
label?: string;
|
||||
|
||||
type?: 'normal' | 'separator' | 'submenu' | 'checkbox' | 'radio';
|
||||
|
||||
accelerator?: string;
|
||||
|
||||
enabled?: boolean;
|
||||
visible?: boolean;
|
||||
checked?: boolean;
|
||||
}
|
||||
|
||||
export interface ISerializableContextMenuItem extends ICommonContextMenuItem {
|
||||
id: number;
|
||||
submenu?: ISerializableContextMenuItem[];
|
||||
}
|
||||
|
||||
export interface IContextMenuItem extends ICommonContextMenuItem {
|
||||
click?: (event: IContextMenuEvent) => void;
|
||||
submenu?: IContextMenuItem[];
|
||||
}
|
||||
|
||||
export interface IContextMenuEvent {
|
||||
shiftKey?: boolean;
|
||||
ctrlKey?: boolean;
|
||||
altKey?: boolean;
|
||||
metaKey?: boolean;
|
||||
}
|
||||
|
||||
export interface IPopupOptions {
|
||||
x?: number;
|
||||
y?: number;
|
||||
positioningItem?: number;
|
||||
onHide?: () => void;
|
||||
}
|
||||
|
||||
export const CONTEXT_MENU_CHANNEL = 'vscode:contextmenu';
|
||||
export const CONTEXT_MENU_CLOSE_CHANNEL = 'vscode:onCloseContextMenu';
|
||||
@@ -0,0 +1,58 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ipcRenderer, Event } from 'electron';
|
||||
import { IContextMenuItem, ISerializableContextMenuItem, CONTEXT_MENU_CLOSE_CHANNEL, CONTEXT_MENU_CHANNEL, IPopupOptions, IContextMenuEvent } from 'vs/base/parts/contextmenu/common/contextmenu';
|
||||
|
||||
let contextMenuIdPool = 0;
|
||||
|
||||
export function popup(items: IContextMenuItem[], options?: IPopupOptions): void {
|
||||
const processedItems: IContextMenuItem[] = [];
|
||||
|
||||
const contextMenuId = contextMenuIdPool++;
|
||||
const onClickChannel = `vscode:onContextMenu${contextMenuId}`;
|
||||
const onClickChannelHandler = (_event: Event, itemId: number, context: IContextMenuEvent) => {
|
||||
const item = processedItems[itemId];
|
||||
if (item.click) {
|
||||
item.click(context);
|
||||
}
|
||||
};
|
||||
|
||||
ipcRenderer.once(onClickChannel, onClickChannelHandler);
|
||||
ipcRenderer.once(CONTEXT_MENU_CLOSE_CHANNEL, (_event: Event, closedContextMenuId: number) => {
|
||||
if (closedContextMenuId !== contextMenuId) {
|
||||
return;
|
||||
}
|
||||
|
||||
ipcRenderer.removeListener(onClickChannel, onClickChannelHandler);
|
||||
|
||||
if (options && options.onHide) {
|
||||
options.onHide();
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.send(CONTEXT_MENU_CHANNEL, contextMenuId, items.map(item => createItem(item, processedItems)), onClickChannel, options);
|
||||
}
|
||||
|
||||
function createItem(item: IContextMenuItem, processedItems: IContextMenuItem[]): ISerializableContextMenuItem {
|
||||
const serializableItem = {
|
||||
id: processedItems.length,
|
||||
label: item.label,
|
||||
type: item.type,
|
||||
accelerator: item.accelerator,
|
||||
checked: item.checked,
|
||||
enabled: typeof item.enabled === 'boolean' ? item.enabled : true,
|
||||
visible: typeof item.visible === 'boolean' ? item.visible : true
|
||||
} as ISerializableContextMenuItem;
|
||||
|
||||
processedItems.push(item);
|
||||
|
||||
// Submenu
|
||||
if (Array.isArray(item.submenu)) {
|
||||
serializableItem.submenu = item.submenu.map(submenuItem => createItem(submenuItem, processedItems));
|
||||
}
|
||||
|
||||
return serializableItem;
|
||||
}
|
||||
63
src/vs/base/parts/contextmenu/electron-main/contextmenu.ts
Normal file
63
src/vs/base/parts/contextmenu/electron-main/contextmenu.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Menu, MenuItem, BrowserWindow, Event, ipcMain } from 'electron';
|
||||
import { ISerializableContextMenuItem, CONTEXT_MENU_CLOSE_CHANNEL, CONTEXT_MENU_CHANNEL, IPopupOptions } from 'vs/base/parts/contextmenu/common/contextmenu';
|
||||
|
||||
export function registerContextMenuListener(): void {
|
||||
ipcMain.on(CONTEXT_MENU_CHANNEL, (event: Event, contextMenuId: number, items: ISerializableContextMenuItem[], onClickChannel: string, options?: IPopupOptions) => {
|
||||
const menu = createMenu(event, onClickChannel, items);
|
||||
|
||||
menu.popup({
|
||||
window: BrowserWindow.fromWebContents(event.sender),
|
||||
x: options ? options.x : void 0,
|
||||
y: options ? options.y : void 0,
|
||||
positioningItem: options ? options.positioningItem : void 0,
|
||||
callback: () => {
|
||||
event.sender.send(CONTEXT_MENU_CLOSE_CHANNEL, contextMenuId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createMenu(event: Event, onClickChannel: string, items: ISerializableContextMenuItem[]): Menu {
|
||||
const menu = new Menu();
|
||||
|
||||
items.forEach(item => {
|
||||
let menuitem: MenuItem;
|
||||
|
||||
// Separator
|
||||
if (item.type === 'separator') {
|
||||
menuitem = new MenuItem({
|
||||
type: item.type,
|
||||
});
|
||||
}
|
||||
|
||||
// Sub Menu
|
||||
else if (Array.isArray(item.submenu)) {
|
||||
menuitem = new MenuItem({
|
||||
submenu: createMenu(event, onClickChannel, item.submenu),
|
||||
label: item.label
|
||||
});
|
||||
}
|
||||
|
||||
// Normal Menu Item
|
||||
else {
|
||||
menuitem = new MenuItem({
|
||||
label: item.label,
|
||||
type: item.type,
|
||||
accelerator: item.accelerator,
|
||||
checked: item.checked,
|
||||
enabled: item.enabled,
|
||||
visible: item.visible,
|
||||
click: (menuItem, win, contextmenuEvent) => event.sender.send(onClickChannel, item.id, contextmenuEvent)
|
||||
});
|
||||
}
|
||||
|
||||
menu.append(menuitem);
|
||||
});
|
||||
|
||||
return menu;
|
||||
}
|
||||
@@ -1,563 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { Promise, TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter, once, filterEvent, toPromise, Relay } from 'vs/base/common/event';
|
||||
|
||||
enum MessageType {
|
||||
RequestPromise,
|
||||
RequestPromiseCancel,
|
||||
ResponseInitialize,
|
||||
ResponsePromiseSuccess,
|
||||
ResponsePromiseProgress,
|
||||
ResponsePromiseError,
|
||||
ResponsePromiseErrorObj,
|
||||
|
||||
RequestEventListen,
|
||||
RequestEventDispose,
|
||||
ResponseEventFire,
|
||||
}
|
||||
|
||||
function isResponse(messageType: MessageType): boolean {
|
||||
return messageType === MessageType.ResponseInitialize
|
||||
|| messageType === MessageType.ResponsePromiseSuccess
|
||||
|| messageType === MessageType.ResponsePromiseProgress
|
||||
|| messageType === MessageType.ResponsePromiseError
|
||||
|| messageType === MessageType.ResponsePromiseErrorObj
|
||||
|| messageType === MessageType.ResponseEventFire;
|
||||
}
|
||||
|
||||
interface IRawMessage {
|
||||
id: number;
|
||||
type: MessageType;
|
||||
}
|
||||
|
||||
interface IRawRequest extends IRawMessage {
|
||||
channelName?: string;
|
||||
name?: string;
|
||||
arg?: any;
|
||||
}
|
||||
|
||||
interface IRequest {
|
||||
raw: IRawRequest;
|
||||
flush?: () => void;
|
||||
}
|
||||
|
||||
interface IRawResponse extends IRawMessage {
|
||||
data: any;
|
||||
}
|
||||
|
||||
interface IHandler {
|
||||
(response: IRawResponse): void;
|
||||
}
|
||||
|
||||
export interface IMessagePassingProtocol {
|
||||
send(request: any): void;
|
||||
onMessage: Event<any>;
|
||||
}
|
||||
|
||||
enum State {
|
||||
Uninitialized,
|
||||
Idle
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IChannel` is an abstraction over a collection of commands.
|
||||
* You can `call` several commands on a channel, each taking at
|
||||
* most one single argument. A `call` always returns a promise
|
||||
* with at most one single return value.
|
||||
*/
|
||||
export interface IChannel {
|
||||
call<T>(command: string, arg?: any): TPromise<T>;
|
||||
listen<T>(event: string, arg?: any): Event<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IChannelServer` hosts a collection of channels. You are
|
||||
* able to register channels onto it, provided a channel name.
|
||||
*/
|
||||
export interface IChannelServer {
|
||||
registerChannel(channelName: string, channel: IChannel): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IChannelClient` has access to a collection of channels. You
|
||||
* are able to get those channels, given their channel name.
|
||||
*/
|
||||
export interface IChannelClient {
|
||||
getChannel<T extends IChannel>(channelName: string): T;
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IClientRouter` is responsible for routing calls to specific
|
||||
* channels, in scenarios in which there are multiple possible
|
||||
* channels (each from a separate client) to pick from.
|
||||
*/
|
||||
export interface IClientRouter {
|
||||
routeCall(command: string, arg: any): TPromise<string>;
|
||||
routeEvent(event: string, arg: any): TPromise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to the `IChannelClient`, you can get channels from this
|
||||
* collection of channels. The difference being that in the
|
||||
* `IRoutingChannelClient`, there are multiple clients providing
|
||||
* the same channel. You'll need to pass in an `IClientRouter` in
|
||||
* order to pick the right one.
|
||||
*/
|
||||
export interface IRoutingChannelClient {
|
||||
getChannel<T extends IChannel>(channelName: string, router: IClientRouter): T;
|
||||
}
|
||||
|
||||
// TODO@joao cleanup this mess!
|
||||
|
||||
export class ChannelServer implements IChannelServer, IDisposable {
|
||||
|
||||
private channels: { [name: string]: IChannel } = Object.create(null);
|
||||
private activeRequests: { [id: number]: IDisposable; } = Object.create(null);
|
||||
private protocolListener: IDisposable;
|
||||
|
||||
constructor(private protocol: IMessagePassingProtocol) {
|
||||
this.protocolListener = this.protocol.onMessage(r => this.onMessage(r));
|
||||
this.protocol.send(<IRawResponse>{ type: MessageType.ResponseInitialize });
|
||||
}
|
||||
|
||||
registerChannel(channelName: string, channel: IChannel): void {
|
||||
this.channels[channelName] = channel;
|
||||
}
|
||||
|
||||
private onMessage(request: IRawRequest): void {
|
||||
switch (request.type) {
|
||||
case MessageType.RequestPromise:
|
||||
this.onPromise(request);
|
||||
break;
|
||||
|
||||
case MessageType.RequestEventListen:
|
||||
this.onEventListen(request);
|
||||
break;
|
||||
|
||||
case MessageType.RequestPromiseCancel:
|
||||
case MessageType.RequestEventDispose:
|
||||
this.disposeActiveRequest(request);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private onPromise(request: IRawRequest): void {
|
||||
const channel = this.channels[request.channelName];
|
||||
let promise: Promise;
|
||||
|
||||
try {
|
||||
promise = channel.call(request.name, request.arg);
|
||||
} catch (err) {
|
||||
promise = TPromise.wrapError(err);
|
||||
}
|
||||
|
||||
const id = request.id;
|
||||
|
||||
const requestPromise = promise.then(data => {
|
||||
this.protocol.send(<IRawResponse>{ id, data, type: MessageType.ResponsePromiseSuccess });
|
||||
delete this.activeRequests[request.id];
|
||||
}, data => {
|
||||
if (data instanceof Error) {
|
||||
this.protocol.send(<IRawResponse>{
|
||||
id, data: {
|
||||
message: data.message,
|
||||
name: data.name,
|
||||
stack: data.stack ? (data.stack.split ? data.stack.split('\n') : data.stack) : void 0
|
||||
}, type: MessageType.ResponsePromiseError
|
||||
});
|
||||
} else {
|
||||
this.protocol.send(<IRawResponse>{ id, data, type: MessageType.ResponsePromiseErrorObj });
|
||||
}
|
||||
|
||||
delete this.activeRequests[request.id];
|
||||
}, data => {
|
||||
this.protocol.send(<IRawResponse>{ id, data, type: MessageType.ResponsePromiseProgress });
|
||||
});
|
||||
|
||||
this.activeRequests[request.id] = toDisposable(() => requestPromise.cancel());
|
||||
}
|
||||
|
||||
private onEventListen(request: IRawRequest): void {
|
||||
const channel = this.channels[request.channelName];
|
||||
|
||||
const id = request.id;
|
||||
const event = channel.listen(request.name, request.arg);
|
||||
const disposable = event(data => this.protocol.send(<IRawResponse>{ id, data, type: MessageType.ResponseEventFire }));
|
||||
|
||||
this.activeRequests[request.id] = disposable;
|
||||
}
|
||||
|
||||
private disposeActiveRequest(request: IRawRequest): void {
|
||||
const disposable = this.activeRequests[request.id];
|
||||
|
||||
if (disposable) {
|
||||
disposable.dispose();
|
||||
delete this.activeRequests[request.id];
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.protocolListener.dispose();
|
||||
this.protocolListener = null;
|
||||
|
||||
Object.keys(this.activeRequests).forEach(id => {
|
||||
this.activeRequests[<any>id].dispose();
|
||||
});
|
||||
|
||||
this.activeRequests = null;
|
||||
}
|
||||
}
|
||||
|
||||
export class ChannelClient implements IChannelClient, IDisposable {
|
||||
|
||||
private state: State = State.Uninitialized;
|
||||
private activeRequests: IDisposable[] = [];
|
||||
private bufferedRequests: IRequest[] = [];
|
||||
private handlers: { [id: number]: IHandler; } = Object.create(null);
|
||||
private lastRequestId: number = 0;
|
||||
private protocolListener: IDisposable;
|
||||
|
||||
private _onDidInitialize = new Emitter<void>();
|
||||
readonly onDidInitialize = this._onDidInitialize.event;
|
||||
|
||||
constructor(private protocol: IMessagePassingProtocol) {
|
||||
this.protocolListener = this.protocol.onMessage(r => this.onMessage(r));
|
||||
}
|
||||
|
||||
getChannel<T extends IChannel>(channelName: string): T {
|
||||
const call = (command: string, arg: any) => this.requestPromise(channelName, command, arg);
|
||||
const listen = (event: string, arg: any) => this.requestEvent(channelName, event, arg);
|
||||
|
||||
return { call, listen } as T;
|
||||
}
|
||||
|
||||
private requestPromise(channelName: string, name: string, arg: any): TPromise<any> {
|
||||
const id = this.lastRequestId++;
|
||||
const type = MessageType.RequestPromise;
|
||||
const request = { raw: { id, type, channelName, name, arg } };
|
||||
|
||||
const activeRequest = this.state === State.Uninitialized
|
||||
? this.bufferRequest(request)
|
||||
: this.doRequest(request);
|
||||
|
||||
const disposable = toDisposable(() => activeRequest.cancel());
|
||||
this.activeRequests.push(disposable);
|
||||
|
||||
activeRequest
|
||||
.then(null, _ => null)
|
||||
.done(() => this.activeRequests = this.activeRequests.filter(el => el !== disposable));
|
||||
|
||||
return activeRequest;
|
||||
}
|
||||
|
||||
private requestEvent(channelName: string, name: string, arg: any): Event<any> {
|
||||
const id = this.lastRequestId++;
|
||||
const type = MessageType.RequestEventListen;
|
||||
const request = { raw: { id, type, channelName, name, arg } };
|
||||
|
||||
let uninitializedPromise: TPromise<any> | null = null;
|
||||
const emitter = new Emitter<any>({
|
||||
onFirstListenerAdd: () => {
|
||||
uninitializedPromise = this.whenInitialized();
|
||||
uninitializedPromise.then(() => {
|
||||
uninitializedPromise = null;
|
||||
this.send(request.raw);
|
||||
});
|
||||
},
|
||||
onLastListenerRemove: () => {
|
||||
if (uninitializedPromise) {
|
||||
uninitializedPromise.cancel();
|
||||
uninitializedPromise = null;
|
||||
} else {
|
||||
this.send({ id, type: MessageType.RequestEventDispose });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.handlers[id] = response => emitter.fire(response.data);
|
||||
return emitter.event;
|
||||
}
|
||||
|
||||
private doRequest(request: IRequest): Promise {
|
||||
const id = request.raw.id;
|
||||
|
||||
return new TPromise((c, e, p) => {
|
||||
this.handlers[id] = response => {
|
||||
switch (response.type) {
|
||||
case MessageType.ResponsePromiseSuccess:
|
||||
delete this.handlers[id];
|
||||
c(response.data);
|
||||
break;
|
||||
|
||||
case MessageType.ResponsePromiseError:
|
||||
delete this.handlers[id];
|
||||
const error = new Error(response.data.message);
|
||||
(<any>error).stack = response.data.stack;
|
||||
error.name = response.data.name;
|
||||
e(error);
|
||||
break;
|
||||
|
||||
case MessageType.ResponsePromiseErrorObj:
|
||||
delete this.handlers[id];
|
||||
e(response.data);
|
||||
break;
|
||||
|
||||
case MessageType.ResponsePromiseProgress:
|
||||
p(response.data);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
this.send(request.raw);
|
||||
},
|
||||
() => this.send({ id, type: MessageType.RequestPromiseCancel }));
|
||||
}
|
||||
|
||||
private bufferRequest(request: IRequest): Promise {
|
||||
let flushedRequest: Promise = null;
|
||||
|
||||
return new TPromise((c, e, p) => {
|
||||
this.bufferedRequests.push(request);
|
||||
|
||||
request.flush = () => {
|
||||
request.flush = null;
|
||||
flushedRequest = this.doRequest(request).then(c, e, p);
|
||||
};
|
||||
}, () => {
|
||||
request.flush = null;
|
||||
|
||||
if (this.state !== State.Uninitialized) {
|
||||
if (flushedRequest) {
|
||||
flushedRequest.cancel();
|
||||
flushedRequest = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = this.bufferedRequests.indexOf(request);
|
||||
|
||||
if (idx === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.bufferedRequests.splice(idx, 1);
|
||||
});
|
||||
}
|
||||
|
||||
private onMessage(response: IRawResponse): void {
|
||||
if (!isResponse(response.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state === State.Uninitialized && response.type === MessageType.ResponseInitialize) {
|
||||
this.state = State.Idle;
|
||||
this._onDidInitialize.fire();
|
||||
this.bufferedRequests.forEach(r => r.flush && r.flush());
|
||||
this.bufferedRequests = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = this.handlers[response.id];
|
||||
if (handler) {
|
||||
handler(response);
|
||||
}
|
||||
}
|
||||
|
||||
private send(raw: IRawRequest) {
|
||||
try {
|
||||
this.protocol.send(raw);
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
private whenInitialized(): TPromise<void> {
|
||||
if (this.state === State.Idle) {
|
||||
return TPromise.as(null);
|
||||
} else {
|
||||
return toPromise(this.onDidInitialize);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.protocolListener.dispose();
|
||||
this.protocolListener = null;
|
||||
|
||||
this.activeRequests = dispose(this.activeRequests);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ClientConnectionEvent {
|
||||
protocol: IMessagePassingProtocol;
|
||||
onDidClientDisconnect: Event<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IPCServer` is both a channel server and a routing channel
|
||||
* client.
|
||||
*
|
||||
* As the owner of a protocol, you should extend both this
|
||||
* and the `IPCClient` classes to get IPC implementations
|
||||
* for your protocol.
|
||||
*/
|
||||
export class IPCServer implements IChannelServer, IRoutingChannelClient, IDisposable {
|
||||
|
||||
private channels: { [name: string]: IChannel } = Object.create(null);
|
||||
private channelClients: { [id: string]: ChannelClient; } = Object.create(null);
|
||||
private onClientAdded = new Emitter<string>();
|
||||
|
||||
constructor(onDidClientConnect: Event<ClientConnectionEvent>) {
|
||||
onDidClientConnect(({ protocol, onDidClientDisconnect }) => {
|
||||
const onFirstMessage = once(protocol.onMessage);
|
||||
|
||||
onFirstMessage(id => {
|
||||
const channelServer = new ChannelServer(protocol);
|
||||
const channelClient = new ChannelClient(protocol);
|
||||
|
||||
Object.keys(this.channels)
|
||||
.forEach(name => channelServer.registerChannel(name, this.channels[name]));
|
||||
|
||||
this.channelClients[id] = channelClient;
|
||||
this.onClientAdded.fire(id);
|
||||
|
||||
onDidClientDisconnect(() => {
|
||||
channelServer.dispose();
|
||||
channelClient.dispose();
|
||||
delete this.channelClients[id];
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getChannel<T extends IChannel>(channelName: string, router: IClientRouter): T {
|
||||
const call = (command: string, arg: any) => {
|
||||
const channelPromise = router.routeCall(command, arg)
|
||||
.then(id => this.getClient(id))
|
||||
.then(client => client.getChannel(channelName));
|
||||
|
||||
return getDelayedChannel(channelPromise)
|
||||
.call(command, arg);
|
||||
};
|
||||
|
||||
const listen = (event: string, arg: any) => {
|
||||
const channelPromise = router.routeEvent(event, arg)
|
||||
.then(id => this.getClient(id))
|
||||
.then(client => client.getChannel(channelName));
|
||||
|
||||
return getDelayedChannel(channelPromise)
|
||||
.listen(event, arg);
|
||||
};
|
||||
|
||||
return { call, listen } as T;
|
||||
}
|
||||
|
||||
registerChannel(channelName: string, channel: IChannel): void {
|
||||
this.channels[channelName] = channel;
|
||||
}
|
||||
|
||||
private getClient(clientId: string): TPromise<IChannelClient> {
|
||||
if (!clientId) {
|
||||
return TPromise.wrapError(new Error('Client id should be provided'));
|
||||
}
|
||||
|
||||
const client = this.channelClients[clientId];
|
||||
|
||||
if (client) {
|
||||
return TPromise.as(client);
|
||||
}
|
||||
|
||||
return new TPromise<IChannelClient>(c => {
|
||||
const onClient = once(filterEvent(this.onClientAdded.event, id => id === clientId));
|
||||
onClient(() => c(this.channelClients[clientId]));
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.channels = Object.create(null);
|
||||
this.channelClients = Object.create(null);
|
||||
this.onClientAdded.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IPCClient` is both a channel client and a channel server.
|
||||
*
|
||||
* As the owner of a protocol, you should extend both this
|
||||
* and the `IPCClient` classes to get IPC implementations
|
||||
* for your protocol.
|
||||
*/
|
||||
export class IPCClient implements IChannelClient, IChannelServer, IDisposable {
|
||||
|
||||
private channelClient: ChannelClient;
|
||||
private channelServer: ChannelServer;
|
||||
|
||||
constructor(protocol: IMessagePassingProtocol, id: string) {
|
||||
protocol.send(id);
|
||||
this.channelClient = new ChannelClient(protocol);
|
||||
this.channelServer = new ChannelServer(protocol);
|
||||
}
|
||||
|
||||
getChannel<T extends IChannel>(channelName: string): T {
|
||||
return this.channelClient.getChannel(channelName) as T;
|
||||
}
|
||||
|
||||
registerChannel(channelName: string, channel: IChannel): void {
|
||||
this.channelServer.registerChannel(channelName, channel);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.channelClient.dispose();
|
||||
this.channelClient = null;
|
||||
this.channelServer.dispose();
|
||||
this.channelServer = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getDelayedChannel<T extends IChannel>(promise: TPromise<T>): T {
|
||||
const call = (command: string, arg: any) => promise.then(c => c.call(command, arg));
|
||||
const listen = (event: string, arg: any) => {
|
||||
const relay = new Relay<any>();
|
||||
promise.then(c => relay.input = c.listen(event, arg));
|
||||
return relay.event;
|
||||
};
|
||||
|
||||
return { call, listen } as T;
|
||||
}
|
||||
|
||||
export function getNextTickChannel<T extends IChannel>(channel: T): T {
|
||||
let didTick = false;
|
||||
|
||||
const call = (command: string, arg: any) => {
|
||||
if (didTick) {
|
||||
return channel.call(command, arg);
|
||||
}
|
||||
|
||||
return TPromise.timeout(0)
|
||||
.then(() => didTick = true)
|
||||
.then(() => channel.call(command, arg));
|
||||
};
|
||||
|
||||
const listen = (event: string, arg: any): Event<any> => {
|
||||
if (didTick) {
|
||||
return channel.listen(event, arg);
|
||||
}
|
||||
|
||||
const relay = new Relay();
|
||||
|
||||
TPromise.timeout(0)
|
||||
.then(() => didTick = true)
|
||||
.then(() => relay.input = channel.listen(event, arg));
|
||||
|
||||
return relay.event;
|
||||
};
|
||||
|
||||
return { call, listen } as T;
|
||||
}
|
||||
@@ -4,19 +4,28 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { fromNodeEventEmitter } from 'vs/base/common/event';
|
||||
import { IPCClient } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Protocol } from 'vs/base/parts/ipc/common/ipc.electron';
|
||||
import { IPCClient } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { Protocol } from 'vs/base/parts/ipc/node/ipc.electron';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export class Client extends IPCClient {
|
||||
export class Client extends IPCClient implements IDisposable {
|
||||
|
||||
private protocol: Protocol;
|
||||
|
||||
private static createProtocol(): Protocol {
|
||||
const onMessage = fromNodeEventEmitter<string>(ipcRenderer, 'ipc:message', (_, message) => message);
|
||||
const onMessage = fromNodeEventEmitter<string>(ipcRenderer, 'ipc:message', (_, message: string) => message);
|
||||
ipcRenderer.send('ipc:hello');
|
||||
return new Protocol(ipcRenderer, onMessage);
|
||||
}
|
||||
|
||||
constructor(id: string) {
|
||||
super(Client.createProtocol(), id);
|
||||
const protocol = Client.createProtocol();
|
||||
super(protocol, id);
|
||||
this.protocol = protocol;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.protocol.dispose();
|
||||
}
|
||||
}
|
||||
@@ -3,35 +3,31 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, filterEvent, mapEvent, fromNodeEventEmitter } from 'vs/base/common/event';
|
||||
import { IPCServer, ClientConnectionEvent } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Protocol } from 'vs/base/parts/ipc/common/ipc.electron';
|
||||
import { Event, filterEvent, mapEvent, fromNodeEventEmitter, signalEvent } from 'vs/base/common/event';
|
||||
import { IPCServer, ClientConnectionEvent } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { Protocol } from 'vs/base/parts/ipc/node/ipc.electron';
|
||||
import { ipcMain } from 'electron';
|
||||
|
||||
interface WebContents extends Electron.WebContents {
|
||||
getId(): number;
|
||||
}
|
||||
|
||||
interface IIPCEvent {
|
||||
event: { sender: WebContents; };
|
||||
event: { sender: Electron.WebContents; };
|
||||
message: string;
|
||||
}
|
||||
|
||||
function createScopedOnMessageEvent(senderId: number): Event<any> {
|
||||
const onMessage = fromNodeEventEmitter<IIPCEvent>(ipcMain, 'ipc:message', (event, message) => ({ event, message }));
|
||||
const onMessageFromSender = filterEvent(onMessage, ({ event }) => event.sender.getId() === senderId);
|
||||
function createScopedOnMessageEvent(senderId: number, eventName: string): Event<string> {
|
||||
const onMessage = fromNodeEventEmitter<IIPCEvent>(ipcMain, eventName, (event, message: string) => ({ event, message }));
|
||||
const onMessageFromSender = filterEvent(onMessage, ({ event }) => event.sender.id === senderId);
|
||||
return mapEvent(onMessageFromSender, ({ message }) => message);
|
||||
}
|
||||
|
||||
export class Server extends IPCServer {
|
||||
|
||||
private static getOnDidClientConnect(): Event<ClientConnectionEvent> {
|
||||
const onHello = fromNodeEventEmitter<WebContents>(ipcMain, 'ipc:hello', ({ sender }) => sender);
|
||||
const onHello = fromNodeEventEmitter<Electron.WebContents>(ipcMain, 'ipc:hello', ({ sender }) => sender);
|
||||
|
||||
return mapEvent(onHello, webContents => {
|
||||
const onMessage = createScopedOnMessageEvent(webContents.getId());
|
||||
const onMessage = createScopedOnMessageEvent(webContents.id, 'ipc:message');
|
||||
const onDidClientDisconnect = signalEvent(createScopedOnMessageEvent(webContents.id, 'ipc:disconnect'));
|
||||
const protocol = new Protocol(webContents, onMessage);
|
||||
const onDidClientDisconnect = fromNodeEventEmitter<void>(webContents, 'destroyed');
|
||||
|
||||
return { protocol, onDidClientDisconnect };
|
||||
});
|
||||
@@ -40,4 +36,4 @@ export class Server extends IPCServer {
|
||||
constructor() {
|
||||
super(Server.getOnDidClientConnect());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,20 +5,32 @@
|
||||
|
||||
import { ChildProcess, fork, ForkOptions } from 'child_process';
|
||||
import { IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Delayer } from 'vs/base/common/async';
|
||||
import { Delayer, always, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { deepClone, assign } from 'vs/base/common/objects';
|
||||
import { Emitter, fromNodeEventEmitter, Event } from 'vs/base/common/event';
|
||||
import { createQueuedSender } from 'vs/base/node/processes';
|
||||
import { ChannelServer as IPCServer, ChannelClient as IPCClient, IChannelClient, IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ChannelServer as IPCServer, ChannelClient as IPCClient, IChannelClient, IChannel } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { isRemoteConsoleLog, log } from 'vs/base/node/console';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
|
||||
export class Server extends IPCServer {
|
||||
constructor() {
|
||||
/**
|
||||
* This implementation doesn't perform well since it uses base64 encoding for buffers.
|
||||
* We should move all implementations to use named ipc.net, so we stop depending on cp.fork.
|
||||
*/
|
||||
|
||||
export class Server<TContext extends string> extends IPCServer<TContext> {
|
||||
constructor(ctx: TContext) {
|
||||
super({
|
||||
send: r => { try { process.send(r); } catch (e) { /* not much to do */ } },
|
||||
onMessage: fromNodeEventEmitter(process, 'message', msg => msg)
|
||||
});
|
||||
send: r => {
|
||||
try {
|
||||
if (process.send) {
|
||||
process.send(r.toString('base64'));
|
||||
}
|
||||
} catch (e) { /* not much to do */ }
|
||||
},
|
||||
onMessage: fromNodeEventEmitter(process, 'message', msg => Buffer.from(msg, 'base64'))
|
||||
}, ctx);
|
||||
|
||||
process.once('disconnect', () => this.dispose());
|
||||
}
|
||||
@@ -74,10 +86,10 @@ export interface IIPCOptions {
|
||||
export class Client implements IChannelClient, IDisposable {
|
||||
|
||||
private disposeDelayer: Delayer<void>;
|
||||
private activeRequests: IDisposable[];
|
||||
private child: ChildProcess;
|
||||
private _client: IPCClient;
|
||||
private channels: { [name: string]: IChannel };
|
||||
private activeRequests = new Set<IDisposable>();
|
||||
private child: ChildProcess | null;
|
||||
private _client: IPCClient | null;
|
||||
private channels = new Map<string, IChannel>();
|
||||
|
||||
private _onDidProcessExit = new Emitter<{ code: number, signal: string }>();
|
||||
readonly onDidProcessExit = this._onDidProcessExit.event;
|
||||
@@ -85,49 +97,54 @@ export class Client implements IChannelClient, IDisposable {
|
||||
constructor(private modulePath: string, private options: IIPCOptions) {
|
||||
const timeout = options && options.timeout ? options.timeout : 60000;
|
||||
this.disposeDelayer = new Delayer<void>(timeout);
|
||||
this.activeRequests = [];
|
||||
this.child = null;
|
||||
this._client = null;
|
||||
this.channels = Object.create(null);
|
||||
}
|
||||
|
||||
getChannel<T extends IChannel>(channelName: string): T {
|
||||
const call = (command: string, arg: any) => this.requestPromise(channelName, command, arg);
|
||||
const listen = (event: string, arg: any) => this.requestEvent(channelName, event, arg);
|
||||
return { call, listen } as IChannel as T;
|
||||
const that = this;
|
||||
|
||||
return {
|
||||
call<T>(command: string, arg?: any, cancellationToken?: CancellationToken): Thenable<T> {
|
||||
return that.requestPromise<T>(channelName, command, arg, cancellationToken);
|
||||
},
|
||||
listen(event: string, arg?: any) {
|
||||
return that.requestEvent(channelName, event, arg);
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
protected requestPromise(channelName: string, name: string, arg: any): TPromise<void> {
|
||||
protected requestPromise<T>(channelName: string, name: string, arg?: any, cancellationToken = CancellationToken.None): Thenable<T> {
|
||||
if (!this.disposeDelayer) {
|
||||
return TPromise.wrapError(new Error('disposed'));
|
||||
return Promise.reject(new Error('disposed'));
|
||||
}
|
||||
|
||||
if (cancellationToken.isCancellationRequested) {
|
||||
return Promise.reject(errors.canceled());
|
||||
}
|
||||
|
||||
this.disposeDelayer.cancel();
|
||||
|
||||
const channel = this.channels[channelName] || (this.channels[channelName] = this.client.getChannel(channelName));
|
||||
const request: TPromise<void> = channel.call(name, arg);
|
||||
|
||||
// Progress doesn't propagate across 'then', we need to create a promise wrapper
|
||||
const result = new TPromise<void>((c, e, p) => {
|
||||
request.then(c, e, p).done(() => {
|
||||
if (!this.activeRequests) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeRequests.splice(this.activeRequests.indexOf(disposable), 1);
|
||||
|
||||
if (this.activeRequests.length === 0) {
|
||||
this.disposeDelayer.trigger(() => this.disposeClient());
|
||||
}
|
||||
});
|
||||
}, () => request.cancel());
|
||||
const channel = this.getCachedChannel(channelName);
|
||||
const result = createCancelablePromise(token => channel.call<T>(name, arg, token));
|
||||
const cancellationTokenListener = cancellationToken.onCancellationRequested(() => result.cancel());
|
||||
|
||||
const disposable = toDisposable(() => result.cancel());
|
||||
this.activeRequests.push(disposable);
|
||||
this.activeRequests.add(disposable);
|
||||
|
||||
always(result, () => {
|
||||
cancellationTokenListener.dispose();
|
||||
this.activeRequests.delete(disposable);
|
||||
|
||||
if (this.activeRequests.size === 0) {
|
||||
this.disposeDelayer.trigger(() => this.disposeClient());
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected requestEvent<T>(channelName: string, name: string, arg: any): Event<T> {
|
||||
protected requestEvent<T>(channelName: string, name: string, arg?: any): Event<T> {
|
||||
if (!this.disposeDelayer) {
|
||||
return Event.None;
|
||||
}
|
||||
@@ -137,22 +154,17 @@ export class Client implements IChannelClient, IDisposable {
|
||||
let listener: IDisposable;
|
||||
const emitter = new Emitter<any>({
|
||||
onFirstListenerAdd: () => {
|
||||
const channel = this.channels[channelName] || (this.channels[channelName] = this.client.getChannel(channelName));
|
||||
const channel = this.getCachedChannel(channelName);
|
||||
const event: Event<T> = channel.listen(name, arg);
|
||||
|
||||
listener = event(emitter.fire, emitter);
|
||||
this.activeRequests.push(listener);
|
||||
|
||||
this.activeRequests.add(listener);
|
||||
},
|
||||
onLastListenerRemove: () => {
|
||||
if (!this.activeRequests) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeRequests.splice(this.activeRequests.indexOf(listener), 1);
|
||||
this.activeRequests.delete(listener);
|
||||
listener.dispose();
|
||||
|
||||
if (this.activeRequests.length === 0) {
|
||||
if (this.activeRequests.size === 0 && this.disposeDelayer) {
|
||||
this.disposeDelayer.trigger(() => this.disposeClient());
|
||||
}
|
||||
}
|
||||
@@ -186,7 +198,7 @@ export class Client implements IChannelClient, IDisposable {
|
||||
|
||||
this.child = fork(this.modulePath, args, forkOpts);
|
||||
|
||||
const onMessageEmitter = new Emitter<any>();
|
||||
const onMessageEmitter = new Emitter<Buffer>();
|
||||
const onRawMessage = fromNodeEventEmitter(this.child, 'message', msg => msg);
|
||||
|
||||
onRawMessage(msg => {
|
||||
@@ -194,15 +206,15 @@ export class Client implements IChannelClient, IDisposable {
|
||||
// Handle remote console logs specially
|
||||
if (isRemoteConsoleLog(msg)) {
|
||||
log(msg, `IPC Library: ${this.options.serverName}`);
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Anything else goes to the outside
|
||||
onMessageEmitter.fire(msg);
|
||||
onMessageEmitter.fire(Buffer.from(msg, 'base64'));
|
||||
});
|
||||
|
||||
const sender = this.options.useQueue ? createQueuedSender(this.child) : this.child;
|
||||
const send = r => this.child && this.child.connected && sender.send(r);
|
||||
const send = (r: Buffer) => this.child && this.child.connected && sender.send(r.toString('base64'));
|
||||
const onMessage = onMessageEmitter.event;
|
||||
const protocol = { send, onMessage };
|
||||
|
||||
@@ -216,16 +228,17 @@ export class Client implements IChannelClient, IDisposable {
|
||||
this.child.on('exit', (code: any, signal: any) => {
|
||||
process.removeListener('exit', onExit);
|
||||
|
||||
if (this.activeRequests) {
|
||||
this.activeRequests = dispose(this.activeRequests);
|
||||
}
|
||||
this.activeRequests.forEach(r => dispose(r));
|
||||
this.activeRequests.clear();
|
||||
|
||||
if (code !== 0 && signal !== 'SIGTERM') {
|
||||
console.warn('IPC "' + this.options.serverName + '" crashed with exit code ' + code + ' and signal ' + signal);
|
||||
this.disposeDelayer.cancel();
|
||||
this.disposeClient();
|
||||
}
|
||||
|
||||
if (this.disposeDelayer) {
|
||||
this.disposeDelayer.cancel();
|
||||
}
|
||||
this.disposeClient();
|
||||
this._onDidProcessExit.fire({ code, signal });
|
||||
});
|
||||
}
|
||||
@@ -233,20 +246,33 @@ export class Client implements IChannelClient, IDisposable {
|
||||
return this._client;
|
||||
}
|
||||
|
||||
private getCachedChannel(name: string): IChannel {
|
||||
let channel = this.channels.get(name);
|
||||
|
||||
if (!channel) {
|
||||
channel = this.client.getChannel(name);
|
||||
this.channels.set(name, channel);
|
||||
}
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
private disposeClient() {
|
||||
if (this._client) {
|
||||
this.child.kill();
|
||||
this.child = null;
|
||||
if (this.child) {
|
||||
this.child.kill();
|
||||
this.child = null;
|
||||
}
|
||||
this._client = null;
|
||||
this.channels = Object.create(null);
|
||||
this.channels.clear();
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._onDidProcessExit.dispose();
|
||||
this.disposeDelayer.cancel();
|
||||
this.disposeDelayer = null;
|
||||
this.disposeDelayer = null!; // StrictNullOverride: nulling out ok in dispose
|
||||
this.disposeClient();
|
||||
this.activeRequests = null;
|
||||
this.activeRequests.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,35 +4,39 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
|
||||
/**
|
||||
* This implementation doesn't perform well since it uses base64 encoding for buffers.
|
||||
* Electron 3.0 should have suport for buffers in IPC: https://github.com/electron/electron/pull/13055
|
||||
*/
|
||||
|
||||
export interface Sender {
|
||||
send(channel: string, ...args: any[]): void;
|
||||
send(channel: string, msg: string | null): void;
|
||||
}
|
||||
|
||||
export class Protocol implements IMessagePassingProtocol {
|
||||
|
||||
private listener: IDisposable;
|
||||
|
||||
private _onMessage: Event<any>;
|
||||
get onMessage(): Event<any> { return this._onMessage; }
|
||||
private _onMessage = new Emitter<Buffer>();
|
||||
get onMessage(): Event<Buffer> { return this._onMessage.event; }
|
||||
|
||||
constructor(private sender: Sender, onMessageEvent: Event<any>) {
|
||||
const emitter = new Emitter<any>();
|
||||
onMessageEvent(msg => emitter.fire(msg));
|
||||
this._onMessage = emitter.event;
|
||||
constructor(private sender: Sender, onMessageEvent: Event<string>) {
|
||||
onMessageEvent(msg => this._onMessage.fire(Buffer.from(msg, 'base64')));
|
||||
}
|
||||
|
||||
send(message: any): void {
|
||||
send(message: Buffer): void {
|
||||
try {
|
||||
this.sender.send('ipc:message', message);
|
||||
this.sender.send('ipc:message', message.toString('base64'));
|
||||
} catch (e) {
|
||||
// systems are going down
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.sender.send('ipc:disconnect', null);
|
||||
this.listener = dispose(this.listener);
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,9 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { Socket, Server as NetServer, createConnection, createServer } from 'net';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Event, Emitter, once, mapEvent, fromNodeEventEmitter } from 'vs/base/common/event';
|
||||
import { IMessagePassingProtocol, ClientConnectionEvent, IPCServer, IPCClient } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IMessagePassingProtocol, ClientConnectionEvent, IPCServer, IPCClient } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
@@ -25,9 +22,17 @@ export function generateRandomPipeName(): string {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A message has the following format:
|
||||
*
|
||||
* [bodyLen|message]
|
||||
* [header^|data^^^]
|
||||
* [u32be^^|buffer^]
|
||||
*/
|
||||
|
||||
export class Protocol implements IDisposable, IMessagePassingProtocol {
|
||||
|
||||
private static readonly _headerLen = 5;
|
||||
private static readonly _headerLen = 4;
|
||||
|
||||
private _isDisposed: boolean;
|
||||
private _chunks: Buffer[];
|
||||
@@ -37,8 +42,8 @@ export class Protocol implements IDisposable, IMessagePassingProtocol {
|
||||
private _socketEndListener: () => void;
|
||||
private _socketCloseListener: () => void;
|
||||
|
||||
private _onMessage = new Emitter<any>();
|
||||
readonly onMessage: Event<any> = this._onMessage.event;
|
||||
private _onMessage = new Emitter<Buffer>();
|
||||
readonly onMessage: Event<Buffer> = this._onMessage.event;
|
||||
|
||||
private _onClose = new Emitter<void>();
|
||||
readonly onClose: Event<void> = this._onClose.event;
|
||||
@@ -51,7 +56,6 @@ export class Protocol implements IDisposable, IMessagePassingProtocol {
|
||||
|
||||
const state = {
|
||||
readHead: true,
|
||||
bodyIsJson: false,
|
||||
bodyLen: -1,
|
||||
};
|
||||
|
||||
@@ -68,8 +72,7 @@ export class Protocol implements IDisposable, IMessagePassingProtocol {
|
||||
if (totalLength >= Protocol._headerLen) {
|
||||
const all = Buffer.concat(this._chunks);
|
||||
|
||||
state.bodyIsJson = all.readInt8(0) === 1;
|
||||
state.bodyLen = all.readInt32BE(1);
|
||||
state.bodyLen = all.readUInt32BE(0);
|
||||
state.readHead = false;
|
||||
|
||||
const rest = all.slice(Protocol._headerLen);
|
||||
@@ -87,21 +90,17 @@ export class Protocol implements IDisposable, IMessagePassingProtocol {
|
||||
if (totalLength >= state.bodyLen) {
|
||||
|
||||
const all = Buffer.concat(this._chunks);
|
||||
let message = all.toString('utf8', 0, state.bodyLen);
|
||||
if (state.bodyIsJson) {
|
||||
message = JSON.parse(message);
|
||||
}
|
||||
const buffer = all.slice(0, state.bodyLen);
|
||||
|
||||
// ensure the public getBuffer returns a valid value if invoked from the event listeners
|
||||
// ensure the getBuffer returns a valid value if invoked from the event listeners
|
||||
const rest = all.slice(state.bodyLen);
|
||||
totalLength = rest.length;
|
||||
this._chunks = [rest];
|
||||
|
||||
state.bodyIsJson = false;
|
||||
state.bodyLen = -1;
|
||||
state.readHead = true;
|
||||
|
||||
this._onMessage.fire(message);
|
||||
this._onMessage.fire(buffer);
|
||||
|
||||
if (this._isDisposed) {
|
||||
// check if an event listener lead to our disposal
|
||||
@@ -117,7 +116,7 @@ export class Protocol implements IDisposable, IMessagePassingProtocol {
|
||||
const acceptFirstDataChunk = () => {
|
||||
if (firstDataChunk && firstDataChunk.length > 0) {
|
||||
let tmp = firstDataChunk;
|
||||
firstDataChunk = null;
|
||||
firstDataChunk = undefined;
|
||||
acceptChunk(tmp);
|
||||
}
|
||||
};
|
||||
@@ -145,7 +144,7 @@ export class Protocol implements IDisposable, IMessagePassingProtocol {
|
||||
_socket.once('close', this._socketCloseListener);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
dispose(): void {
|
||||
this._isDisposed = true;
|
||||
this._firstChunkTimer.dispose();
|
||||
this._socket.removeListener('data', this._socketDataListener);
|
||||
@@ -153,30 +152,18 @@ export class Protocol implements IDisposable, IMessagePassingProtocol {
|
||||
this._socket.removeListener('close', this._socketCloseListener);
|
||||
}
|
||||
|
||||
public end(): void {
|
||||
end(): void {
|
||||
this._socket.end();
|
||||
}
|
||||
|
||||
public getBuffer(): Buffer {
|
||||
getBuffer(): Buffer {
|
||||
return Buffer.concat(this._chunks);
|
||||
}
|
||||
|
||||
public send(message: any): void {
|
||||
|
||||
// [bodyIsJson|bodyLen|message]
|
||||
// |^header^^^^^^^^^^^|^data^^]
|
||||
|
||||
const header = Buffer.alloc(Protocol._headerLen);
|
||||
|
||||
// ensure string
|
||||
if (typeof message !== 'string') {
|
||||
message = JSON.stringify(message);
|
||||
header.writeInt8(1, 0, true);
|
||||
}
|
||||
const data = Buffer.from(message);
|
||||
header.writeInt32BE(data.length, 1, true);
|
||||
|
||||
this._writeSoon(header, data);
|
||||
send(buffer: Buffer): void {
|
||||
const header = Buffer.allocUnsafe(Protocol._headerLen);
|
||||
header.writeUInt32BE(buffer.length, 0, true);
|
||||
this._writeSoon(header, buffer);
|
||||
}
|
||||
|
||||
private _writeBuffer = new class {
|
||||
@@ -228,26 +215,31 @@ export class Server extends IPCServer {
|
||||
}));
|
||||
}
|
||||
|
||||
constructor(private server: NetServer) {
|
||||
private server: NetServer | null;
|
||||
|
||||
constructor(server: NetServer) {
|
||||
super(Server.toClientConnectionEvent(server));
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
this.server.close();
|
||||
this.server = null;
|
||||
if (this.server) {
|
||||
this.server.close();
|
||||
this.server = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Client extends IPCClient {
|
||||
export class Client<TContext = string> extends IPCClient<TContext> {
|
||||
|
||||
public static fromSocket(socket: Socket, id: string): Client {
|
||||
static fromSocket<TContext = string>(socket: Socket, id: TContext): Client<TContext> {
|
||||
return new Client(new Protocol(socket), id);
|
||||
}
|
||||
|
||||
get onClose(): Event<void> { return this.protocol.onClose; }
|
||||
|
||||
constructor(private protocol: Protocol, id: string) {
|
||||
constructor(private protocol: Protocol, id: TContext) {
|
||||
super(protocol, id);
|
||||
}
|
||||
|
||||
@@ -257,10 +249,10 @@ export class Client extends IPCClient {
|
||||
}
|
||||
}
|
||||
|
||||
export function serve(port: number): TPromise<Server>;
|
||||
export function serve(namedPipe: string): TPromise<Server>;
|
||||
export function serve(hook: any): TPromise<Server> {
|
||||
return new TPromise<Server>((c, e) => {
|
||||
export function serve(port: number): Thenable<Server>;
|
||||
export function serve(namedPipe: string): Thenable<Server>;
|
||||
export function serve(hook: any): Thenable<Server> {
|
||||
return new Promise<Server>((c, e) => {
|
||||
const server = createServer();
|
||||
|
||||
server.on('error', e);
|
||||
@@ -271,11 +263,11 @@ export function serve(hook: any): TPromise<Server> {
|
||||
});
|
||||
}
|
||||
|
||||
export function connect(options: { host: string, port: number }, clientId: string): TPromise<Client>;
|
||||
export function connect(port: number, clientId: string): TPromise<Client>;
|
||||
export function connect(namedPipe: string, clientId: string): TPromise<Client>;
|
||||
export function connect(hook: any, clientId: string): TPromise<Client> {
|
||||
return new TPromise<Client>((c, e) => {
|
||||
export function connect(options: { host: string, port: number }, clientId: string): Thenable<Client>;
|
||||
export function connect(port: number, clientId: string): Thenable<Client>;
|
||||
export function connect(namedPipe: string, clientId: string): Thenable<Client>;
|
||||
export function connect(hook: any, clientId: string): Thenable<Client> {
|
||||
return new Promise<Client>((c, e) => {
|
||||
const socket = createConnection(hook, () => {
|
||||
socket.removeListener('error', e);
|
||||
c(Client.fromSocket(socket, clientId));
|
||||
|
||||
749
src/vs/base/parts/ipc/node/ipc.ts
Normal file
749
src/vs/base/parts/ipc/node/ipc.ts
Normal file
@@ -0,0 +1,749 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter, once, toPromise, Relay } from 'vs/base/common/event';
|
||||
import { always, CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
|
||||
export const enum RequestType {
|
||||
Promise = 100,
|
||||
PromiseCancel = 101,
|
||||
EventListen = 102,
|
||||
EventDispose = 103
|
||||
}
|
||||
|
||||
type IRawPromiseRequest = { type: RequestType.Promise; id: number; channelName: string; name: string; arg: any; };
|
||||
type IRawPromiseCancelRequest = { type: RequestType.PromiseCancel, id: number };
|
||||
type IRawEventListenRequest = { type: RequestType.EventListen; id: number; channelName: string; name: string; arg: any; };
|
||||
type IRawEventDisposeRequest = { type: RequestType.EventDispose, id: number };
|
||||
type IRawRequest = IRawPromiseRequest | IRawPromiseCancelRequest | IRawEventListenRequest | IRawEventDisposeRequest;
|
||||
|
||||
export const enum ResponseType {
|
||||
Initialize = 200,
|
||||
PromiseSuccess = 201,
|
||||
PromiseError = 202,
|
||||
PromiseErrorObj = 203,
|
||||
EventFire = 204
|
||||
}
|
||||
|
||||
type IRawInitializeResponse = { type: ResponseType.Initialize };
|
||||
type IRawPromiseSuccessResponse = { type: ResponseType.PromiseSuccess; id: number; data: any };
|
||||
type IRawPromiseErrorResponse = { type: ResponseType.PromiseError; id: number; data: { message: string, name: string, stack: string[] | undefined } };
|
||||
type IRawPromiseErrorObjResponse = { type: ResponseType.PromiseErrorObj; id: number; data: any };
|
||||
type IRawEventFireResponse = { type: ResponseType.EventFire; id: number; data: any };
|
||||
type IRawResponse = IRawInitializeResponse | IRawPromiseSuccessResponse | IRawPromiseErrorResponse | IRawPromiseErrorObjResponse | IRawEventFireResponse;
|
||||
|
||||
interface IHandler {
|
||||
(response: IRawResponse): void;
|
||||
}
|
||||
|
||||
export interface IMessagePassingProtocol {
|
||||
send(buffer: Buffer): void;
|
||||
onMessage: Event<Buffer>;
|
||||
}
|
||||
|
||||
enum State {
|
||||
Uninitialized,
|
||||
Idle
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IChannel` is an abstraction over a collection of commands.
|
||||
* You can `call` several commands on a channel, each taking at
|
||||
* most one single argument. A `call` always returns a promise
|
||||
* with at most one single return value.
|
||||
*/
|
||||
export interface IChannel {
|
||||
call<T>(command: string, arg?: any, cancellationToken?: CancellationToken): Thenable<T>;
|
||||
listen<T>(event: string, arg?: any): Event<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IServerChannel` is the couter part to `IChannel`,
|
||||
* on the server-side. You should implement this interface
|
||||
* if you'd like to handle remote promises or events.
|
||||
*/
|
||||
export interface IServerChannel<TContext = string> {
|
||||
call<T>(ctx: TContext, command: string, arg?: any, cancellationToken?: CancellationToken): Thenable<T>;
|
||||
listen<T>(ctx: TContext, event: string, arg?: any): Event<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IChannelServer` hosts a collection of channels. You are
|
||||
* able to register channels onto it, provided a channel name.
|
||||
*/
|
||||
export interface IChannelServer<TContext = string> {
|
||||
registerChannel(channelName: string, channel: IServerChannel<TContext>): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IChannelClient` has access to a collection of channels. You
|
||||
* are able to get those channels, given their channel name.
|
||||
*/
|
||||
export interface IChannelClient {
|
||||
getChannel<T extends IChannel>(channelName: string): T;
|
||||
}
|
||||
|
||||
export interface Client<TContext> {
|
||||
readonly ctx: TContext;
|
||||
}
|
||||
|
||||
export interface IConnectionHub<TContext> {
|
||||
readonly connections: Connection<TContext>[];
|
||||
readonly onDidChangeConnections: Event<Connection<TContext>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IClientRouter` is responsible for routing calls to specific
|
||||
* channels, in scenarios in which there are multiple possible
|
||||
* channels (each from a separate client) to pick from.
|
||||
*/
|
||||
export interface IClientRouter<TContext = string> {
|
||||
routeCall(hub: IConnectionHub<TContext>, command: string, arg?: any, cancellationToken?: CancellationToken): Thenable<Client<TContext>>;
|
||||
routeEvent(hub: IConnectionHub<TContext>, event: string, arg?: any): Thenable<Client<TContext>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to the `IChannelClient`, you can get channels from this
|
||||
* collection of channels. The difference being that in the
|
||||
* `IRoutingChannelClient`, there are multiple clients providing
|
||||
* the same channel. You'll need to pass in an `IClientRouter` in
|
||||
* order to pick the right one.
|
||||
*/
|
||||
export interface IRoutingChannelClient<TContext = string> {
|
||||
getChannel<T extends IChannel>(channelName: string, router: IClientRouter<TContext>): T;
|
||||
}
|
||||
|
||||
interface IReader {
|
||||
read(bytes: number): Buffer;
|
||||
}
|
||||
|
||||
interface IWriter {
|
||||
write(buffer: Buffer): void;
|
||||
}
|
||||
|
||||
class BufferReader implements IReader {
|
||||
|
||||
private pos = 0;
|
||||
|
||||
constructor(private buffer: Buffer) { }
|
||||
|
||||
read(bytes: number): Buffer {
|
||||
const result = this.buffer.slice(this.pos, this.pos + bytes);
|
||||
this.pos += result.length;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class BufferWriter implements IWriter {
|
||||
|
||||
private buffers: Buffer[] = [];
|
||||
|
||||
get buffer(): Buffer {
|
||||
return Buffer.concat(this.buffers);
|
||||
}
|
||||
|
||||
write(buffer: Buffer): void {
|
||||
this.buffers.push(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
enum DataType {
|
||||
Undefined = 0,
|
||||
String = 1,
|
||||
Buffer = 2,
|
||||
Array = 3,
|
||||
Object = 4
|
||||
}
|
||||
|
||||
function createSizeBuffer(size: number): Buffer {
|
||||
const result = Buffer.allocUnsafe(4);
|
||||
result.writeUInt32BE(size, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
function readSizeBuffer(reader: IReader): number {
|
||||
return reader.read(4).readUInt32BE(0);
|
||||
}
|
||||
|
||||
const BufferPresets = {
|
||||
Undefined: Buffer.alloc(1, DataType.Undefined),
|
||||
String: Buffer.alloc(1, DataType.String),
|
||||
Buffer: Buffer.alloc(1, DataType.Buffer),
|
||||
Array: Buffer.alloc(1, DataType.Array),
|
||||
Object: Buffer.alloc(1, DataType.Object)
|
||||
};
|
||||
|
||||
function serialize(writer: IWriter, data: any): void {
|
||||
if (typeof data === 'undefined') {
|
||||
writer.write(BufferPresets.Undefined);
|
||||
} else if (typeof data === 'string') {
|
||||
const buffer = Buffer.from(data);
|
||||
writer.write(BufferPresets.String);
|
||||
writer.write(createSizeBuffer(buffer.length));
|
||||
writer.write(buffer);
|
||||
} else if (Buffer.isBuffer(data)) {
|
||||
writer.write(BufferPresets.Buffer);
|
||||
writer.write(createSizeBuffer(data.length));
|
||||
writer.write(data);
|
||||
} else if (Array.isArray(data)) {
|
||||
writer.write(BufferPresets.Array);
|
||||
writer.write(createSizeBuffer(data.length));
|
||||
|
||||
for (const el of data) {
|
||||
serialize(writer, el);
|
||||
}
|
||||
} else {
|
||||
const buffer = Buffer.from(JSON.stringify(data));
|
||||
writer.write(BufferPresets.Object);
|
||||
writer.write(createSizeBuffer(buffer.length));
|
||||
writer.write(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize(reader: IReader): any {
|
||||
const type = reader.read(1).readUInt8(0);
|
||||
|
||||
switch (type) {
|
||||
case DataType.Undefined: return undefined;
|
||||
case DataType.String: return reader.read(readSizeBuffer(reader)).toString();
|
||||
case DataType.Buffer: return reader.read(readSizeBuffer(reader));
|
||||
case DataType.Array: {
|
||||
const length = readSizeBuffer(reader);
|
||||
const result: any[] = [];
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result.push(deserialize(reader));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
case DataType.Object: return JSON.parse(reader.read(readSizeBuffer(reader)).toString());
|
||||
}
|
||||
}
|
||||
|
||||
export class ChannelServer<TContext = string> implements IChannelServer<TContext>, IDisposable {
|
||||
|
||||
private channels = new Map<string, IServerChannel<TContext>>();
|
||||
private activeRequests = new Map<number, IDisposable>();
|
||||
private protocolListener: IDisposable | null;
|
||||
|
||||
constructor(private protocol: IMessagePassingProtocol, private ctx: TContext) {
|
||||
this.protocolListener = this.protocol.onMessage(msg => this.onRawMessage(msg));
|
||||
this.sendResponse({ type: ResponseType.Initialize });
|
||||
}
|
||||
|
||||
registerChannel(channelName: string, channel: IServerChannel<TContext>): void {
|
||||
this.channels.set(channelName, channel);
|
||||
}
|
||||
|
||||
private sendResponse(response: IRawResponse): void {
|
||||
switch (response.type) {
|
||||
case ResponseType.Initialize:
|
||||
return this.send([response.type]);
|
||||
|
||||
case ResponseType.PromiseSuccess:
|
||||
case ResponseType.PromiseError:
|
||||
case ResponseType.EventFire:
|
||||
case ResponseType.PromiseErrorObj:
|
||||
return this.send([response.type, response.id], response.data);
|
||||
}
|
||||
}
|
||||
|
||||
private send(header: any, body: any = undefined): void {
|
||||
const writer = new BufferWriter();
|
||||
serialize(writer, header);
|
||||
serialize(writer, body);
|
||||
this.sendBuffer(writer.buffer);
|
||||
}
|
||||
|
||||
private sendBuffer(message: Buffer): void {
|
||||
try {
|
||||
this.protocol.send(message);
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
private onRawMessage(message: Buffer): void {
|
||||
const reader = new BufferReader(message);
|
||||
const header = deserialize(reader);
|
||||
const body = deserialize(reader);
|
||||
const type = header[0] as RequestType;
|
||||
|
||||
switch (type) {
|
||||
case RequestType.Promise:
|
||||
return this.onPromise({ type, id: header[1], channelName: header[2], name: header[3], arg: body });
|
||||
case RequestType.EventListen:
|
||||
return this.onEventListen({ type, id: header[1], channelName: header[2], name: header[3], arg: body });
|
||||
case RequestType.PromiseCancel:
|
||||
return this.disposeActiveRequest({ type, id: header[1] });
|
||||
case RequestType.EventDispose:
|
||||
return this.disposeActiveRequest({ type, id: header[1] });
|
||||
}
|
||||
}
|
||||
|
||||
private onPromise(request: IRawPromiseRequest): void {
|
||||
const channel = this.channels.get(request.channelName);
|
||||
const cancellationTokenSource = new CancellationTokenSource();
|
||||
let promise: Thenable<any>;
|
||||
|
||||
try {
|
||||
promise = channel.call(this.ctx, request.name, request.arg, cancellationTokenSource.token);
|
||||
} catch (err) {
|
||||
promise = Promise.reject(err);
|
||||
}
|
||||
|
||||
const id = request.id;
|
||||
|
||||
promise.then(data => {
|
||||
this.sendResponse(<IRawResponse>{ id, data, type: ResponseType.PromiseSuccess });
|
||||
this.activeRequests.delete(request.id);
|
||||
}, err => {
|
||||
if (err instanceof Error) {
|
||||
this.sendResponse(<IRawResponse>{
|
||||
id, data: {
|
||||
message: err.message,
|
||||
name: err.name,
|
||||
stack: err.stack ? (err.stack.split ? err.stack.split('\n') : err.stack) : void 0
|
||||
}, type: ResponseType.PromiseError
|
||||
});
|
||||
} else {
|
||||
this.sendResponse(<IRawResponse>{ id, data: err, type: ResponseType.PromiseErrorObj });
|
||||
}
|
||||
|
||||
this.activeRequests.delete(request.id);
|
||||
});
|
||||
|
||||
const disposable = toDisposable(() => cancellationTokenSource.cancel());
|
||||
this.activeRequests.set(request.id, disposable);
|
||||
}
|
||||
|
||||
private onEventListen(request: IRawEventListenRequest): void {
|
||||
const channel = this.channels.get(request.channelName);
|
||||
|
||||
const id = request.id;
|
||||
const event = channel.listen(this.ctx, request.name, request.arg);
|
||||
const disposable = event(data => this.sendResponse(<IRawResponse>{ id, data, type: ResponseType.EventFire }));
|
||||
|
||||
this.activeRequests.set(request.id, disposable);
|
||||
}
|
||||
|
||||
private disposeActiveRequest(request: IRawRequest): void {
|
||||
const disposable = this.activeRequests.get(request.id);
|
||||
|
||||
if (disposable) {
|
||||
disposable.dispose();
|
||||
this.activeRequests.delete(request.id);
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this.protocolListener) {
|
||||
this.protocolListener.dispose();
|
||||
this.protocolListener = null;
|
||||
}
|
||||
this.activeRequests.forEach(d => d.dispose());
|
||||
this.activeRequests.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class ChannelClient implements IChannelClient, IDisposable {
|
||||
|
||||
private state: State = State.Uninitialized;
|
||||
private activeRequests = new Set<IDisposable>();
|
||||
private handlers = new Map<number, IHandler>();
|
||||
private lastRequestId: number = 0;
|
||||
private protocolListener: IDisposable | null;
|
||||
|
||||
private _onDidInitialize = new Emitter<void>();
|
||||
readonly onDidInitialize = this._onDidInitialize.event;
|
||||
|
||||
constructor(private protocol: IMessagePassingProtocol) {
|
||||
this.protocolListener = this.protocol.onMessage(msg => this.onBuffer(msg));
|
||||
}
|
||||
|
||||
getChannel<T extends IChannel>(channelName: string): T {
|
||||
const that = this;
|
||||
|
||||
return {
|
||||
call(command: string, arg?: any, cancellationToken?: CancellationToken) {
|
||||
return that.requestPromise(channelName, command, arg, cancellationToken);
|
||||
},
|
||||
listen(event: string, arg: any) {
|
||||
return that.requestEvent(channelName, event, arg);
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
private requestPromise(channelName: string, name: string, arg?: any, cancellationToken = CancellationToken.None): Thenable<any> {
|
||||
const id = this.lastRequestId++;
|
||||
const type = RequestType.Promise;
|
||||
const request: IRawRequest = { id, type, channelName, name, arg };
|
||||
|
||||
if (cancellationToken.isCancellationRequested) {
|
||||
return Promise.reject(errors.canceled());
|
||||
}
|
||||
|
||||
let disposable: IDisposable;
|
||||
|
||||
const result = new Promise((c, e) => {
|
||||
if (cancellationToken.isCancellationRequested) {
|
||||
return e(errors.canceled());
|
||||
}
|
||||
|
||||
let uninitializedPromise: CancelablePromise<void> | null = createCancelablePromise(_ => this.whenInitialized());
|
||||
uninitializedPromise.then(() => {
|
||||
uninitializedPromise = null;
|
||||
|
||||
const handler: IHandler = response => {
|
||||
switch (response.type) {
|
||||
case ResponseType.PromiseSuccess:
|
||||
this.handlers.delete(id);
|
||||
c(response.data);
|
||||
break;
|
||||
|
||||
case ResponseType.PromiseError:
|
||||
this.handlers.delete(id);
|
||||
const error = new Error(response.data.message);
|
||||
(<any>error).stack = response.data.stack;
|
||||
error.name = response.data.name;
|
||||
e(error);
|
||||
break;
|
||||
|
||||
case ResponseType.PromiseErrorObj:
|
||||
this.handlers.delete(id);
|
||||
e(response.data);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
this.handlers.set(id, handler);
|
||||
this.sendRequest(request);
|
||||
});
|
||||
|
||||
const cancel = () => {
|
||||
if (uninitializedPromise) {
|
||||
uninitializedPromise.cancel();
|
||||
uninitializedPromise = null;
|
||||
} else {
|
||||
this.sendRequest({ id, type: RequestType.PromiseCancel });
|
||||
}
|
||||
|
||||
e(errors.canceled());
|
||||
};
|
||||
|
||||
const cancellationTokenListener = cancellationToken.onCancellationRequested(cancel);
|
||||
disposable = combinedDisposable([toDisposable(cancel), cancellationTokenListener]);
|
||||
this.activeRequests.add(disposable);
|
||||
});
|
||||
|
||||
always(result, () => this.activeRequests.delete(disposable));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private requestEvent(channelName: string, name: string, arg?: any): Event<any> {
|
||||
const id = this.lastRequestId++;
|
||||
const type = RequestType.EventListen;
|
||||
const request: IRawRequest = { id, type, channelName, name, arg };
|
||||
|
||||
let uninitializedPromise: CancelablePromise<void> | null = null;
|
||||
|
||||
const emitter = new Emitter<any>({
|
||||
onFirstListenerAdd: () => {
|
||||
uninitializedPromise = createCancelablePromise(_ => this.whenInitialized());
|
||||
uninitializedPromise.then(() => {
|
||||
uninitializedPromise = null;
|
||||
this.activeRequests.add(emitter);
|
||||
this.sendRequest(request);
|
||||
});
|
||||
},
|
||||
onLastListenerRemove: () => {
|
||||
if (uninitializedPromise) {
|
||||
uninitializedPromise.cancel();
|
||||
uninitializedPromise = null;
|
||||
} else {
|
||||
this.activeRequests.delete(emitter);
|
||||
this.sendRequest({ id, type: RequestType.EventDispose });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const handler: IHandler = (res: IRawEventFireResponse) => emitter.fire(res.data);
|
||||
this.handlers.set(id, handler);
|
||||
|
||||
return emitter.event;
|
||||
}
|
||||
|
||||
private sendRequest(request: IRawRequest): void {
|
||||
switch (request.type) {
|
||||
case RequestType.Promise:
|
||||
case RequestType.EventListen:
|
||||
return this.send([request.type, request.id, request.channelName, request.name], request.arg);
|
||||
|
||||
case RequestType.PromiseCancel:
|
||||
case RequestType.EventDispose:
|
||||
return this.send([request.type, request.id]);
|
||||
}
|
||||
}
|
||||
|
||||
private send(header: any, body: any = undefined): void {
|
||||
const writer = new BufferWriter();
|
||||
serialize(writer, header);
|
||||
serialize(writer, body);
|
||||
this.sendBuffer(writer.buffer);
|
||||
}
|
||||
|
||||
private sendBuffer(message: Buffer): void {
|
||||
try {
|
||||
this.protocol.send(message);
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
private onBuffer(message: Buffer): void {
|
||||
const reader = new BufferReader(message);
|
||||
const header = deserialize(reader);
|
||||
const body = deserialize(reader);
|
||||
const type: ResponseType = header[0];
|
||||
|
||||
switch (type) {
|
||||
case ResponseType.Initialize:
|
||||
return this.onResponse({ type: header[0] });
|
||||
|
||||
case ResponseType.PromiseSuccess:
|
||||
case ResponseType.PromiseError:
|
||||
case ResponseType.EventFire:
|
||||
case ResponseType.PromiseErrorObj:
|
||||
return this.onResponse({ type: header[0], id: header[1], data: body });
|
||||
}
|
||||
}
|
||||
|
||||
private onResponse(response: IRawResponse): void {
|
||||
if (response.type === ResponseType.Initialize) {
|
||||
this.state = State.Idle;
|
||||
this._onDidInitialize.fire();
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = this.handlers.get(response.id);
|
||||
|
||||
if (handler) {
|
||||
handler(response);
|
||||
}
|
||||
}
|
||||
|
||||
private whenInitialized(): Thenable<void> {
|
||||
if (this.state === State.Idle) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return toPromise(this.onDidInitialize);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.protocolListener) {
|
||||
this.protocolListener.dispose();
|
||||
this.protocolListener = null;
|
||||
}
|
||||
this.activeRequests.forEach(p => p.dispose());
|
||||
this.activeRequests.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export interface ClientConnectionEvent {
|
||||
protocol: IMessagePassingProtocol;
|
||||
onDidClientDisconnect: Event<void>;
|
||||
}
|
||||
|
||||
interface Connection<TContext> extends Client<TContext> {
|
||||
readonly channelClient: ChannelClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IPCServer` is both a channel server and a routing channel
|
||||
* client.
|
||||
*
|
||||
* As the owner of a protocol, you should extend both this
|
||||
* and the `IPCClient` classes to get IPC implementations
|
||||
* for your protocol.
|
||||
*/
|
||||
export class IPCServer<TContext = string> implements IChannelServer<TContext>, IRoutingChannelClient<TContext>, IConnectionHub<TContext>, IDisposable {
|
||||
|
||||
private channels = new Map<string, IServerChannel<TContext>>();
|
||||
private _connections = new Set<Connection<TContext>>();
|
||||
|
||||
private _onDidChangeConnections = new Emitter<Connection<TContext>>();
|
||||
readonly onDidChangeConnections: Event<Connection<TContext>> = this._onDidChangeConnections.event;
|
||||
|
||||
get connections(): Connection<TContext>[] {
|
||||
const result: Connection<TContext>[] = [];
|
||||
this._connections.forEach(ctx => result.push(ctx));
|
||||
return result;
|
||||
}
|
||||
|
||||
constructor(onDidClientConnect: Event<ClientConnectionEvent>) {
|
||||
onDidClientConnect(({ protocol, onDidClientDisconnect }) => {
|
||||
const onFirstMessage = once(protocol.onMessage);
|
||||
|
||||
onFirstMessage(msg => {
|
||||
const reader = new BufferReader(msg);
|
||||
const ctx = deserialize(reader) as TContext;
|
||||
|
||||
const channelServer = new ChannelServer(protocol, ctx);
|
||||
const channelClient = new ChannelClient(protocol);
|
||||
|
||||
this.channels.forEach((channel, name) => channelServer.registerChannel(name, channel));
|
||||
|
||||
const connection: Connection<TContext> = { channelClient, ctx };
|
||||
this._connections.add(connection);
|
||||
this._onDidChangeConnections.fire(connection);
|
||||
|
||||
onDidClientDisconnect(() => {
|
||||
channelServer.dispose();
|
||||
channelClient.dispose();
|
||||
this._connections.delete(connection);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getChannel<T extends IChannel>(channelName: string, router: IClientRouter<TContext>): T {
|
||||
const that = this;
|
||||
|
||||
return {
|
||||
call(command: string, arg?: any, cancellationToken?: CancellationToken) {
|
||||
const channelPromise = router.routeCall(that, command, arg)
|
||||
.then(connection => (connection as Connection<TContext>).channelClient.getChannel(channelName));
|
||||
|
||||
return getDelayedChannel(channelPromise)
|
||||
.call(command, arg, cancellationToken);
|
||||
},
|
||||
listen(event: string, arg: any) {
|
||||
const channelPromise = router.routeEvent(that, event, arg)
|
||||
.then(connection => (connection as Connection<TContext>).channelClient.getChannel(channelName));
|
||||
|
||||
return getDelayedChannel(channelPromise)
|
||||
.listen(event, arg);
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
registerChannel(channelName: string, channel: IServerChannel<TContext>): void {
|
||||
this.channels.set(channelName, channel);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.channels.clear();
|
||||
this._connections.clear();
|
||||
this._onDidChangeConnections.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An `IPCClient` is both a channel client and a channel server.
|
||||
*
|
||||
* As the owner of a protocol, you should extend both this
|
||||
* and the `IPCClient` classes to get IPC implementations
|
||||
* for your protocol.
|
||||
*/
|
||||
export class IPCClient<TContext = string> implements IChannelClient, IChannelServer<TContext>, IDisposable {
|
||||
|
||||
private channelClient: ChannelClient;
|
||||
private channelServer: ChannelServer<TContext>;
|
||||
|
||||
constructor(protocol: IMessagePassingProtocol, ctx: TContext) {
|
||||
const writer = new BufferWriter();
|
||||
serialize(writer, ctx);
|
||||
protocol.send(writer.buffer);
|
||||
|
||||
this.channelClient = new ChannelClient(protocol);
|
||||
this.channelServer = new ChannelServer(protocol, ctx);
|
||||
}
|
||||
|
||||
getChannel<T extends IChannel>(channelName: string): T {
|
||||
return this.channelClient.getChannel(channelName) as T;
|
||||
}
|
||||
|
||||
registerChannel(channelName: string, channel: IServerChannel<TContext>): void {
|
||||
this.channelServer.registerChannel(channelName, channel);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.channelClient.dispose();
|
||||
this.channelServer.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export function getDelayedChannel<T extends IChannel>(promise: Thenable<T>): T {
|
||||
return {
|
||||
call(command: string, arg?: any, cancellationToken?: CancellationToken): Thenable<T> {
|
||||
return promise.then(c => c.call(command, arg, cancellationToken));
|
||||
},
|
||||
|
||||
listen<T>(event: string, arg?: any): Event<T> {
|
||||
const relay = new Relay<any>();
|
||||
promise.then(c => relay.input = c.listen(event, arg));
|
||||
return relay.event;
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
export function getNextTickChannel<T extends IChannel>(channel: T): T {
|
||||
let didTick = false;
|
||||
|
||||
return {
|
||||
call<T>(command: string, arg?: any, cancellationToken?: CancellationToken): Thenable<T> {
|
||||
if (didTick) {
|
||||
return channel.call(command, arg, cancellationToken);
|
||||
}
|
||||
|
||||
return timeout(0)
|
||||
.then(() => didTick = true)
|
||||
.then(() => channel.call<T>(command, arg, cancellationToken));
|
||||
},
|
||||
listen<T>(event: string, arg?: any): Event<T> {
|
||||
if (didTick) {
|
||||
return channel.listen<T>(event, arg);
|
||||
}
|
||||
|
||||
const relay = new Relay<T>();
|
||||
|
||||
timeout(0)
|
||||
.then(() => didTick = true)
|
||||
.then(() => relay.input = channel.listen<T>(event, arg));
|
||||
|
||||
return relay.event;
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
export class StaticRouter<TContext = string> implements IClientRouter<TContext> {
|
||||
|
||||
constructor(private fn: (ctx: TContext) => boolean | Thenable<boolean>) { }
|
||||
|
||||
routeCall(hub: IConnectionHub<TContext>): Thenable<Client<TContext>> {
|
||||
return this.route(hub);
|
||||
}
|
||||
|
||||
routeEvent(hub: IConnectionHub<TContext>): Thenable<Client<TContext>> {
|
||||
return this.route(hub);
|
||||
}
|
||||
|
||||
private async route(hub: IConnectionHub<TContext>): Promise<Client<TContext>> {
|
||||
for (const connection of hub.connections) {
|
||||
if (await Promise.resolve(this.fn(connection.ctx))) {
|
||||
return Promise.resolve(connection);
|
||||
}
|
||||
}
|
||||
|
||||
await toPromise(hub.onDidChangeConnections);
|
||||
return await this.route(hub);
|
||||
}
|
||||
}
|
||||
79
src/vs/base/parts/ipc/test/node/ipc.cp.test.ts
Normal file
79
src/vs/base/parts/ipc/test/node/ipc.cp.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { always } from 'vs/base/common/async';
|
||||
import { TestServiceClient } from './testService';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
|
||||
function createClient(): Client {
|
||||
return new Client(getPathFromAmdModule(require, 'bootstrap-fork'), {
|
||||
serverName: 'TestServer',
|
||||
env: { AMD_ENTRYPOINT: 'vs/base/parts/ipc/test/node/testApp', verbose: true }
|
||||
});
|
||||
}
|
||||
|
||||
suite('IPC, Child Process', () => {
|
||||
test('createChannel', () => {
|
||||
const client = createClient();
|
||||
const channel = client.getChannel('test');
|
||||
const service = new TestServiceClient(channel);
|
||||
|
||||
const result = service.pong('ping').then(r => {
|
||||
assert.equal(r.incoming, 'ping');
|
||||
assert.equal(r.outgoing, 'pong');
|
||||
});
|
||||
|
||||
return always(result, () => client.dispose());
|
||||
});
|
||||
|
||||
test('events', () => {
|
||||
const client = createClient();
|
||||
const channel = client.getChannel('test');
|
||||
const service = new TestServiceClient(channel);
|
||||
|
||||
const event = new Promise((c, e) => {
|
||||
service.onMarco(({ answer }) => {
|
||||
try {
|
||||
assert.equal(answer, 'polo');
|
||||
c(null);
|
||||
} catch (err) {
|
||||
e(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const request = service.marco();
|
||||
const result = Promise.all([request, event]);
|
||||
|
||||
return always(result, () => client.dispose());
|
||||
});
|
||||
|
||||
test('event dispose', () => {
|
||||
const client = createClient();
|
||||
const channel = client.getChannel('test');
|
||||
const service = new TestServiceClient(channel);
|
||||
|
||||
let count = 0;
|
||||
const disposable = service.onMarco(() => count++);
|
||||
|
||||
const result = service.marco().then(async answer => {
|
||||
assert.equal(answer, 'polo');
|
||||
assert.equal(count, 1);
|
||||
|
||||
const answer_1 = await service.marco();
|
||||
assert.equal(answer_1, 'polo');
|
||||
assert.equal(count, 2);
|
||||
disposable.dispose();
|
||||
|
||||
const answer_2 = await service.marco();
|
||||
assert.equal(answer_2, 'polo');
|
||||
assert.equal(count, 2);
|
||||
});
|
||||
|
||||
return always(result, () => client.dispose());
|
||||
});
|
||||
});
|
||||
@@ -3,10 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Socket } from 'net';
|
||||
import { EventEmitter } from 'events';
|
||||
import { Protocol } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
@@ -41,27 +38,28 @@ suite('IPC, Socket Protocol', () => {
|
||||
stream = <any>new MockDuplex();
|
||||
});
|
||||
|
||||
test('read/write', () => {
|
||||
test('read/write', async () => {
|
||||
|
||||
const a = new Protocol(stream);
|
||||
const b = new Protocol(stream);
|
||||
|
||||
return new TPromise(resolve => {
|
||||
await new Promise(resolve => {
|
||||
const sub = b.onMessage(data => {
|
||||
sub.dispose();
|
||||
assert.equal(data, 'foobarfarboo');
|
||||
assert.equal(data.toString(), 'foobarfarboo');
|
||||
resolve(null);
|
||||
});
|
||||
a.send('foobarfarboo');
|
||||
}).then(() => {
|
||||
return new TPromise(resolve => {
|
||||
const sub = b.onMessage(data => {
|
||||
sub.dispose();
|
||||
assert.equal(data, 123);
|
||||
resolve(null);
|
||||
});
|
||||
a.send(123);
|
||||
a.send(Buffer.from('foobarfarboo'));
|
||||
});
|
||||
return new Promise(resolve => {
|
||||
const sub_1 = b.onMessage(data => {
|
||||
sub_1.dispose();
|
||||
assert.equal(data.readInt8(0), 123);
|
||||
resolve(null);
|
||||
});
|
||||
const buffer = Buffer.allocUnsafe(1);
|
||||
buffer.writeInt8(123, 0);
|
||||
a.send(buffer);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,11 +76,11 @@ suite('IPC, Socket Protocol', () => {
|
||||
data: 'Hello World'.split('')
|
||||
};
|
||||
|
||||
a.send(data);
|
||||
a.send(Buffer.from(JSON.stringify(data)));
|
||||
|
||||
return new TPromise(resolve => {
|
||||
return new Promise(resolve => {
|
||||
b.onMessage(msg => {
|
||||
assert.deepEqual(msg, data);
|
||||
assert.deepEqual(JSON.parse(msg.toString()), data);
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
@@ -90,7 +88,7 @@ suite('IPC, Socket Protocol', () => {
|
||||
|
||||
test('can devolve to a socket and evolve again without losing data', () => {
|
||||
let resolve: (v: void) => void;
|
||||
let result = new TPromise<void>((_resolve, _reject) => {
|
||||
let result = new Promise<void>((_resolve, _reject) => {
|
||||
resolve = _resolve;
|
||||
});
|
||||
const sender = new Protocol(stream);
|
||||
@@ -100,7 +98,7 @@ suite('IPC, Socket Protocol', () => {
|
||||
assert.equal(stream.listenerCount('end'), 2);
|
||||
|
||||
receiver1.onMessage((msg) => {
|
||||
assert.equal(msg.value, 1);
|
||||
assert.equal(JSON.parse(msg.toString()).value, 1);
|
||||
|
||||
let buffer = receiver1.getBuffer();
|
||||
receiver1.dispose();
|
||||
@@ -110,15 +108,15 @@ suite('IPC, Socket Protocol', () => {
|
||||
|
||||
const receiver2 = new Protocol(stream, buffer);
|
||||
receiver2.onMessage((msg) => {
|
||||
assert.equal(msg.value, 2);
|
||||
assert.equal(JSON.parse(msg.toString()).value, 2);
|
||||
resolve(void 0);
|
||||
});
|
||||
});
|
||||
|
||||
const msg1 = { value: 1 };
|
||||
const msg2 = { value: 2 };
|
||||
sender.send(msg1);
|
||||
sender.send(msg2);
|
||||
sender.send(Buffer.from(JSON.stringify(msg1)));
|
||||
sender.send(Buffer.from(JSON.stringify(msg2)));
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import uri from 'vs/base/common/uri';
|
||||
import { always } from 'vs/base/common/async';
|
||||
import { ITestChannel, TestServiceClient, ITestService } from './testService';
|
||||
|
||||
function createClient(): Client {
|
||||
return new Client(uri.parse(require.toUrl('bootstrap')).fsPath, {
|
||||
serverName: 'TestServer',
|
||||
env: { AMD_ENTRYPOINT: 'vs/base/parts/ipc/test/node/testApp', verbose: true }
|
||||
});
|
||||
}
|
||||
|
||||
// Rename to ipc.perf.test.ts and run with ./scripts/test.sh --grep IPC.performance --timeout 60000
|
||||
suite('IPC performance', () => {
|
||||
|
||||
test('increasing batch size', () => {
|
||||
const client = createClient();
|
||||
const channel = client.getChannel<ITestChannel>('test');
|
||||
const service = new TestServiceClient(channel);
|
||||
|
||||
const runs = [
|
||||
{ batches: 250000, size: 1 },
|
||||
{ batches: 2500, size: 100 },
|
||||
{ batches: 500, size: 500 },
|
||||
{ batches: 250, size: 1000 },
|
||||
{ batches: 50, size: 5000 },
|
||||
{ batches: 25, size: 10000 },
|
||||
// { batches: 10, size: 25000 },
|
||||
// { batches: 5, size: 50000 },
|
||||
// { batches: 1, size: 250000 },
|
||||
];
|
||||
const dataSizes = [
|
||||
100,
|
||||
250,
|
||||
];
|
||||
let i = 0, j = 0;
|
||||
const result = measure(service, 10, 10, 250) // warm-up
|
||||
.then(() => {
|
||||
return (function nextRun() {
|
||||
if (i >= runs.length) {
|
||||
if (++j >= dataSizes.length) {
|
||||
return;
|
||||
}
|
||||
i = 0;
|
||||
}
|
||||
const run = runs[i++];
|
||||
return measure(service, run.batches, run.size, dataSizes[j])
|
||||
.then(() => {
|
||||
return nextRun();
|
||||
});
|
||||
})();
|
||||
});
|
||||
|
||||
return always(result, () => client.dispose());
|
||||
});
|
||||
|
||||
test('increasing raw data size', () => {
|
||||
const client = createClient();
|
||||
const channel = client.getChannel<ITestChannel>('test');
|
||||
const service = new TestServiceClient(channel);
|
||||
|
||||
const runs = [
|
||||
{ batches: 250000, dataSize: 100 },
|
||||
{ batches: 25000, dataSize: 1000 },
|
||||
{ batches: 2500, dataSize: 10000 },
|
||||
{ batches: 1250, dataSize: 20000 },
|
||||
{ batches: 500, dataSize: 50000 },
|
||||
{ batches: 250, dataSize: 100000 },
|
||||
{ batches: 125, dataSize: 200000 },
|
||||
{ batches: 50, dataSize: 500000 },
|
||||
{ batches: 25, dataSize: 1000000 },
|
||||
];
|
||||
let i = 0;
|
||||
const result = measure(service, 10, 10, 250) // warm-up
|
||||
.then(() => {
|
||||
return (function nextRun() {
|
||||
if (i >= runs.length) {
|
||||
return;
|
||||
}
|
||||
const run = runs[i++];
|
||||
return measure(service, run.batches, 1, run.dataSize)
|
||||
.then(() => {
|
||||
return nextRun();
|
||||
});
|
||||
})();
|
||||
});
|
||||
|
||||
return always(result, () => client.dispose());
|
||||
});
|
||||
|
||||
function measure(service: ITestService, batches: number, size: number, dataSize: number) {
|
||||
const start = Date.now();
|
||||
let hits = 0;
|
||||
let count = 0;
|
||||
return service.batchPerf(batches, size, dataSize)
|
||||
.then(() => {
|
||||
console.log(`Batches: ${batches}, size: ${size}, dataSize: ${dataSize}, n: ${batches * size * dataSize}, duration: ${Date.now() - start}`);
|
||||
assert.strictEqual(hits, batches);
|
||||
assert.strictEqual(count, batches * size);
|
||||
}, err => assert.fail(err),
|
||||
batch => {
|
||||
hits++;
|
||||
count += batch.length;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -3,102 +3,300 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import uri from 'vs/base/common/uri';
|
||||
import { always } from 'vs/base/common/async';
|
||||
import { isPromiseCanceledError } from 'vs/base/common/errors';
|
||||
import { ITestChannel, TestServiceClient } from './testService';
|
||||
import { IMessagePassingProtocol, IPCServer, ClientConnectionEvent, IPCClient, IChannel, IServerChannel } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { Emitter, toPromise, Event } from 'vs/base/common/event';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { canceled } from 'vs/base/common/errors';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
|
||||
function createClient(): Client {
|
||||
return new Client(uri.parse(require.toUrl('bootstrap')).fsPath, {
|
||||
serverName: 'TestServer',
|
||||
env: { AMD_ENTRYPOINT: 'vs/base/parts/ipc/test/node/testApp', verbose: true }
|
||||
class QueueProtocol implements IMessagePassingProtocol {
|
||||
|
||||
private buffering = true;
|
||||
private buffers: Buffer[] = [];
|
||||
|
||||
private _onMessage = new Emitter<Buffer>({
|
||||
onFirstListenerDidAdd: () => {
|
||||
for (const buffer of this.buffers) {
|
||||
this._onMessage.fire(buffer);
|
||||
}
|
||||
|
||||
this.buffers = [];
|
||||
this.buffering = false;
|
||||
},
|
||||
onLastListenerRemove: () => {
|
||||
this.buffering = true;
|
||||
}
|
||||
});
|
||||
|
||||
readonly onMessage = this._onMessage.event;
|
||||
other: QueueProtocol;
|
||||
|
||||
send(buffer: Buffer): void {
|
||||
this.other.receive(buffer);
|
||||
}
|
||||
|
||||
protected receive(buffer: Buffer): void {
|
||||
if (this.buffering) {
|
||||
this.buffers.push(buffer);
|
||||
} else {
|
||||
this._onMessage.fire(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suite('IPC', () => {
|
||||
suite('child process', () => {
|
||||
function createProtocolPair(): [IMessagePassingProtocol, IMessagePassingProtocol] {
|
||||
const one = new QueueProtocol();
|
||||
const other = new QueueProtocol();
|
||||
one.other = other;
|
||||
other.other = one;
|
||||
|
||||
test('createChannel', () => {
|
||||
const client = createClient();
|
||||
const channel = client.getChannel<ITestChannel>('test');
|
||||
const service = new TestServiceClient(channel);
|
||||
return [one, other];
|
||||
}
|
||||
|
||||
const result = service.pong('ping').then(r => {
|
||||
assert.equal(r.incoming, 'ping');
|
||||
assert.equal(r.outgoing, 'pong');
|
||||
});
|
||||
class TestIPCClient extends IPCClient<string> {
|
||||
|
||||
return always(result, () => client.dispose());
|
||||
private _onDidDisconnect = new Emitter<void>();
|
||||
readonly onDidDisconnect = this._onDidDisconnect.event;
|
||||
|
||||
constructor(protocol: IMessagePassingProtocol, id: string) {
|
||||
super(protocol, id);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._onDidDisconnect.fire();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class TestIPCServer extends IPCServer<string> {
|
||||
|
||||
private onDidClientConnect: Emitter<ClientConnectionEvent>;
|
||||
|
||||
constructor() {
|
||||
const onDidClientConnect = new Emitter<ClientConnectionEvent>();
|
||||
super(onDidClientConnect.event);
|
||||
this.onDidClientConnect = onDidClientConnect;
|
||||
}
|
||||
|
||||
createConnection(id: string): IPCClient<string> {
|
||||
const [pc, ps] = createProtocolPair();
|
||||
const client = new TestIPCClient(pc, id);
|
||||
|
||||
this.onDidClientConnect.fire({
|
||||
protocol: ps,
|
||||
onDidClientDisconnect: client.onDidDisconnect
|
||||
});
|
||||
|
||||
test('cancellation', () => {
|
||||
const client = createClient();
|
||||
const channel = client.getChannel<ITestChannel>('test');
|
||||
const service = new TestServiceClient(channel);
|
||||
const res = service.cancelMe();
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => res.cancel(), 50);
|
||||
const TestChannelId = 'testchannel';
|
||||
|
||||
const result = res.then(
|
||||
() => assert.fail('Unexpected'),
|
||||
err => assert.ok(err && isPromiseCanceledError(err))
|
||||
interface ITestService {
|
||||
marco(): Thenable<string>;
|
||||
error(message: string): Thenable<void>;
|
||||
neverComplete(): Thenable<void>;
|
||||
neverCompleteCT(cancellationToken: CancellationToken): Thenable<void>;
|
||||
buffersLength(buffers: Buffer[]): Thenable<number>;
|
||||
|
||||
pong: Event<string>;
|
||||
}
|
||||
|
||||
class TestService implements ITestService {
|
||||
|
||||
private _pong = new Emitter<string>();
|
||||
readonly pong = this._pong.event;
|
||||
|
||||
marco(): Thenable<string> {
|
||||
return Promise.resolve('polo');
|
||||
}
|
||||
|
||||
error(message: string): Thenable<void> {
|
||||
return Promise.reject(new Error(message));
|
||||
}
|
||||
|
||||
neverComplete(): Thenable<void> {
|
||||
return new Promise(_ => { });
|
||||
}
|
||||
|
||||
neverCompleteCT(cancellationToken: CancellationToken): Thenable<void> {
|
||||
if (cancellationToken.isCancellationRequested) {
|
||||
return Promise.reject(canceled());
|
||||
}
|
||||
|
||||
return new Promise((_, e) => cancellationToken.onCancellationRequested(() => e(canceled())));
|
||||
}
|
||||
|
||||
buffersLength(buffers: Buffer[]): Thenable<number> {
|
||||
return Promise.resolve(buffers.reduce((r, b) => r + b.length, 0));
|
||||
}
|
||||
|
||||
ping(msg: string): void {
|
||||
this._pong.fire(msg);
|
||||
}
|
||||
}
|
||||
|
||||
class TestChannel implements IServerChannel {
|
||||
|
||||
constructor(private service: ITestService) { }
|
||||
|
||||
call(_, command: string, arg?: any, cancellationToken?: CancellationToken): Thenable<any> {
|
||||
switch (command) {
|
||||
case 'marco': return this.service.marco();
|
||||
case 'error': return this.service.error(arg);
|
||||
case 'neverComplete': return this.service.neverComplete();
|
||||
case 'neverCompleteCT': return this.service.neverCompleteCT(cancellationToken);
|
||||
case 'buffersLength': return this.service.buffersLength(arg);
|
||||
default: return Promise.reject(new Error('not implemented'));
|
||||
}
|
||||
}
|
||||
|
||||
listen(_, event: string, arg?: any): Event<any> {
|
||||
switch (event) {
|
||||
case 'pong': return this.service.pong;
|
||||
default: throw new Error('not implemented');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TestChannelClient implements ITestService {
|
||||
|
||||
get pong(): Event<string> {
|
||||
return this.channel.listen('pong');
|
||||
}
|
||||
|
||||
constructor(private channel: IChannel) { }
|
||||
|
||||
marco(): Thenable<string> {
|
||||
return this.channel.call('marco');
|
||||
}
|
||||
|
||||
error(message: string): Thenable<void> {
|
||||
return this.channel.call('error', message);
|
||||
}
|
||||
|
||||
neverComplete(): Thenable<void> {
|
||||
return this.channel.call('neverComplete');
|
||||
}
|
||||
|
||||
neverCompleteCT(cancellationToken: CancellationToken): Thenable<void> {
|
||||
return this.channel.call('neverCompleteCT', undefined, cancellationToken);
|
||||
}
|
||||
|
||||
buffersLength(buffers: Buffer[]): Thenable<number> {
|
||||
return this.channel.call('buffersLength', buffers);
|
||||
}
|
||||
}
|
||||
|
||||
suite('Base IPC', function () {
|
||||
|
||||
test('createProtocolPair', async function () {
|
||||
const [clientProtocol, serverProtocol] = createProtocolPair();
|
||||
|
||||
const b1 = Buffer.alloc(0);
|
||||
clientProtocol.send(b1);
|
||||
|
||||
const b3 = Buffer.alloc(0);
|
||||
serverProtocol.send(b3);
|
||||
|
||||
const b2 = await toPromise(serverProtocol.onMessage);
|
||||
const b4 = await toPromise(clientProtocol.onMessage);
|
||||
|
||||
assert.strictEqual(b1, b2);
|
||||
assert.strictEqual(b3, b4);
|
||||
});
|
||||
|
||||
suite('one to one', function () {
|
||||
let server: IPCServer;
|
||||
let client: IPCClient;
|
||||
let service: TestService;
|
||||
let ipcService: ITestService;
|
||||
|
||||
setup(function () {
|
||||
service = new TestService();
|
||||
const testServer = new TestIPCServer();
|
||||
server = testServer;
|
||||
|
||||
server.registerChannel(TestChannelId, new TestChannel(service));
|
||||
|
||||
client = testServer.createConnection('client1');
|
||||
ipcService = new TestChannelClient(client.getChannel(TestChannelId));
|
||||
});
|
||||
|
||||
teardown(function () {
|
||||
client.dispose();
|
||||
server.dispose();
|
||||
});
|
||||
|
||||
test('call success', async function () {
|
||||
const r = await ipcService.marco();
|
||||
return assert.equal(r, 'polo');
|
||||
});
|
||||
|
||||
test('call error', async function () {
|
||||
try {
|
||||
await ipcService.error('nice error');
|
||||
return assert.fail('should not reach here');
|
||||
} catch (err) {
|
||||
return assert.equal(err.message, 'nice error');
|
||||
}
|
||||
});
|
||||
|
||||
test('cancel call with cancelled cancellation token', async function () {
|
||||
try {
|
||||
await ipcService.neverCompleteCT(CancellationToken.Cancelled);
|
||||
return assert.fail('should not reach here');
|
||||
} catch (err) {
|
||||
return assert(err.message === 'Canceled');
|
||||
}
|
||||
});
|
||||
|
||||
test('cancel call with cancellation token (sync)', function () {
|
||||
const cts = new CancellationTokenSource();
|
||||
const promise = ipcService.neverCompleteCT(cts.token).then(
|
||||
_ => assert.fail('should not reach here'),
|
||||
err => assert(err.message === 'Canceled')
|
||||
);
|
||||
|
||||
return always(result, () => client.dispose());
|
||||
cts.cancel();
|
||||
|
||||
return promise;
|
||||
});
|
||||
|
||||
test('events', () => {
|
||||
const client = createClient();
|
||||
const channel = client.getChannel<ITestChannel>('test');
|
||||
const service = new TestServiceClient(channel);
|
||||
test('cancel call with cancellation token (async)', function () {
|
||||
const cts = new CancellationTokenSource();
|
||||
const promise = ipcService.neverCompleteCT(cts.token).then(
|
||||
_ => assert.fail('should not reach here'),
|
||||
err => assert(err.message === 'Canceled')
|
||||
);
|
||||
|
||||
const event = new TPromise((c, e) => {
|
||||
service.onMarco(({ answer }) => {
|
||||
try {
|
||||
assert.equal(answer, 'polo');
|
||||
c(null);
|
||||
} catch (err) {
|
||||
e(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
setTimeout(() => cts.cancel());
|
||||
|
||||
const request = service.marco();
|
||||
const result = TPromise.join<any>([request, event]);
|
||||
|
||||
return always(result, () => client.dispose());
|
||||
return promise;
|
||||
});
|
||||
|
||||
test('event dispose', () => {
|
||||
const client = createClient();
|
||||
const channel = client.getChannel<ITestChannel>('test');
|
||||
const service = new TestServiceClient(channel);
|
||||
test('listen to events', async function () {
|
||||
const messages: string[] = [];
|
||||
|
||||
let count = 0;
|
||||
const disposable = service.onMarco(() => count++);
|
||||
ipcService.pong(msg => messages.push(msg));
|
||||
await timeout(0);
|
||||
|
||||
const result = service.marco().then(answer => {
|
||||
assert.equal(answer, 'polo');
|
||||
assert.equal(count, 1);
|
||||
assert.deepEqual(messages, []);
|
||||
service.ping('hello');
|
||||
await timeout(0);
|
||||
|
||||
return service.marco().then(answer => {
|
||||
assert.equal(answer, 'polo');
|
||||
assert.equal(count, 2);
|
||||
disposable.dispose();
|
||||
assert.deepEqual(messages, ['hello']);
|
||||
service.ping('world');
|
||||
await timeout(0);
|
||||
|
||||
return service.marco().then(answer => {
|
||||
assert.equal(answer, 'polo');
|
||||
assert.equal(count, 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
assert.deepEqual(messages, ['hello', 'world']);
|
||||
});
|
||||
|
||||
return always(result, () => client.dispose());
|
||||
test('buffers in arrays', async function () {
|
||||
const r = await ipcService.buffersLength([Buffer.allocUnsafe(2), Buffer.allocUnsafe(3)]);
|
||||
return assert.equal(r, 5);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { TestChannel, TestService } from './testService';
|
||||
|
||||
const server = new Server();
|
||||
const server = new Server('test');
|
||||
const service = new TestService();
|
||||
server.registerChannel('test', new TestChannel(service));
|
||||
@@ -2,11 +2,10 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { TPromise, PPromise } from 'vs/base/common/winjs.base';
|
||||
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
|
||||
export interface IMarcoPoloEvent {
|
||||
answer: string;
|
||||
@@ -14,10 +13,9 @@ export interface IMarcoPoloEvent {
|
||||
|
||||
export interface ITestService {
|
||||
onMarco: Event<IMarcoPoloEvent>;
|
||||
marco(): TPromise<string>;
|
||||
pong(ping: string): TPromise<{ incoming: string, outgoing: string }>;
|
||||
cancelMe(): TPromise<boolean>;
|
||||
batchPerf(batches: number, size: number, dataSize: number): PPromise<any, any[]>;
|
||||
marco(): Thenable<string>;
|
||||
pong(ping: string): Thenable<{ incoming: string, outgoing: string }>;
|
||||
cancelMe(): Thenable<boolean>;
|
||||
}
|
||||
|
||||
export class TestService implements ITestService {
|
||||
@@ -25,64 +23,25 @@ export class TestService implements ITestService {
|
||||
private _onMarco = new Emitter<IMarcoPoloEvent>();
|
||||
onMarco: Event<IMarcoPoloEvent> = this._onMarco.event;
|
||||
|
||||
private _data = 'abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
marco(): TPromise<string> {
|
||||
marco(): Thenable<string> {
|
||||
this._onMarco.fire({ answer: 'polo' });
|
||||
return TPromise.as('polo');
|
||||
return Promise.resolve('polo');
|
||||
}
|
||||
|
||||
pong(ping: string): TPromise<{ incoming: string, outgoing: string }> {
|
||||
return TPromise.as({ incoming: ping, outgoing: 'pong' });
|
||||
pong(ping: string): Thenable<{ incoming: string, outgoing: string }> {
|
||||
return Promise.resolve({ incoming: ping, outgoing: 'pong' });
|
||||
}
|
||||
|
||||
cancelMe(): TPromise<boolean> {
|
||||
return TPromise.timeout(100).then(() => true);
|
||||
}
|
||||
|
||||
batchPerf(batches: number, size: number, dataSize: number): PPromise<any, any[]> {
|
||||
while (this._data.length < dataSize) {
|
||||
this._data += this._data;
|
||||
}
|
||||
const self = this;
|
||||
return new PPromise<any, any[]>((complete, error, progress) => {
|
||||
let j = 0;
|
||||
function send() {
|
||||
if (j >= batches) {
|
||||
complete(null);
|
||||
return;
|
||||
}
|
||||
j++;
|
||||
const batch = [];
|
||||
for (let i = 0; i < size; i++) {
|
||||
batch.push({
|
||||
prop: `${i}${self._data}`.substr(0, dataSize)
|
||||
});
|
||||
}
|
||||
progress(batch);
|
||||
process.nextTick(send);
|
||||
}
|
||||
process.nextTick(send);
|
||||
});
|
||||
cancelMe(): Thenable<boolean> {
|
||||
return Promise.resolve(timeout(100)).then(() => true);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ITestChannel extends IChannel {
|
||||
listen<IMarcoPoloEvent>(event: 'marco'): Event<IMarcoPoloEvent>;
|
||||
listen<T>(event: string, arg?: any): Event<T>;
|
||||
|
||||
call(command: 'marco'): TPromise<any>;
|
||||
call(command: 'pong', ping: string): TPromise<any>;
|
||||
call(command: 'cancelMe'): TPromise<any>;
|
||||
call(command: 'batchPerf', args: { batches: number; size: number; dataSize: number; }): PPromise<any, any[]>;
|
||||
call(command: string, ...args: any[]): TPromise<any>;
|
||||
}
|
||||
|
||||
export class TestChannel implements ITestChannel {
|
||||
export class TestChannel implements IServerChannel {
|
||||
|
||||
constructor(private testService: ITestService) { }
|
||||
|
||||
listen(event: string, arg?: any): Event<any> {
|
||||
listen(_, event: string): Event<any> {
|
||||
switch (event) {
|
||||
case 'marco': return this.testService.onMarco;
|
||||
}
|
||||
@@ -90,13 +49,12 @@ export class TestChannel implements ITestChannel {
|
||||
throw new Error('Event not found');
|
||||
}
|
||||
|
||||
call(command: string, ...args: any[]): TPromise<any> {
|
||||
call(_, command: string, ...args: any[]): Thenable<any> {
|
||||
switch (command) {
|
||||
case 'pong': return this.testService.pong(args[0]);
|
||||
case 'cancelMe': return this.testService.cancelMe();
|
||||
case 'marco': return this.testService.marco();
|
||||
case 'batchPerf': return this.testService.batchPerf(args[0].batches, args[0].size, args[0].dataSize);
|
||||
default: return TPromise.wrapError(new Error('command not found'));
|
||||
default: return Promise.reject(new Error(`command not found: ${command}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,21 +63,17 @@ export class TestServiceClient implements ITestService {
|
||||
|
||||
get onMarco(): Event<IMarcoPoloEvent> { return this.channel.listen('marco'); }
|
||||
|
||||
constructor(private channel: ITestChannel) { }
|
||||
constructor(private channel: IChannel) { }
|
||||
|
||||
marco(): TPromise<string> {
|
||||
marco(): Thenable<string> {
|
||||
return this.channel.call('marco');
|
||||
}
|
||||
|
||||
pong(ping: string): TPromise<{ incoming: string, outgoing: string }> {
|
||||
pong(ping: string): Thenable<{ incoming: string, outgoing: string }> {
|
||||
return this.channel.call('pong', ping);
|
||||
}
|
||||
|
||||
cancelMe(): TPromise<boolean> {
|
||||
cancelMe(): Thenable<boolean> {
|
||||
return this.channel.call('cancelMe');
|
||||
}
|
||||
|
||||
batchPerf(batches: number, size: number, dataSize: number): PPromise<any, any[]> {
|
||||
return this.channel.call('batchPerf', { batches, size, dataSize });
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,15 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ITree, IActionProvider } from 'vs/base/parts/tree/browser/tree';
|
||||
import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel';
|
||||
import { IQuickNavigateConfiguration, IModel, IDataSource, IFilter, IAccessiblityProvider, IRenderer, IRunner, Mode } from 'vs/base/parts/quickopen/common/quickOpen';
|
||||
import { Action, IAction, IActionRunner } from 'vs/base/common/actions';
|
||||
import { compareAnything } from 'vs/base/common/comparers';
|
||||
import { ActionBar, IActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { IQuickOpenStyles } from 'vs/base/parts/quickopen/browser/quickOpenWidget';
|
||||
@@ -291,19 +288,19 @@ class NoActionProvider implements IActionProvider {
|
||||
return false;
|
||||
}
|
||||
|
||||
getActions(tree: ITree, element: any): TPromise<IAction[]> {
|
||||
return TPromise.as(null);
|
||||
getActions(tree: ITree, element: any): IAction[] {
|
||||
return null;
|
||||
}
|
||||
|
||||
hasSecondaryActions(tree: ITree, element: any): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getSecondaryActions(tree: ITree, element: any): TPromise<IAction[]> {
|
||||
return TPromise.as(null);
|
||||
getSecondaryActions(tree: ITree, element: any): IAction[] {
|
||||
return null;
|
||||
}
|
||||
|
||||
getActionItem(tree: ITree, element: any, action: Action): IActionItem {
|
||||
getActionItem(tree: ITree, element: any, action: Action) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -330,7 +327,7 @@ class Renderer implements IRenderer<QuickOpenEntry> {
|
||||
private actionProvider: IActionProvider;
|
||||
private actionRunner: IActionRunner;
|
||||
|
||||
constructor(actionProvider: IActionProvider = new NoActionProvider(), actionRunner: IActionRunner = null) {
|
||||
constructor(actionProvider: IActionProvider = new NoActionProvider(), actionRunner: IActionRunner | null = null) {
|
||||
this.actionProvider = actionProvider;
|
||||
this.actionRunner = actionRunner;
|
||||
}
|
||||
@@ -379,7 +376,7 @@ class Renderer implements IRenderer<QuickOpenEntry> {
|
||||
const detailContainer = document.createElement('div');
|
||||
row2.appendChild(detailContainer);
|
||||
DOM.addClass(detailContainer, 'quick-open-entry-meta');
|
||||
const detail = new HighlightedLabel(detailContainer);
|
||||
const detail = new HighlightedLabel(detailContainer, true);
|
||||
|
||||
// Entry Group
|
||||
let group: HTMLDivElement;
|
||||
@@ -423,13 +420,12 @@ class Renderer implements IRenderer<QuickOpenEntry> {
|
||||
|
||||
data.actionBar.context = entry; // make sure the context is the current element
|
||||
|
||||
this.actionProvider.getActions(null, entry).then((actions) => {
|
||||
if (data.actionBar.isEmpty() && actions && actions.length > 0) {
|
||||
data.actionBar.push(actions, { icon: true, label: false });
|
||||
} else if (!data.actionBar.isEmpty() && (!actions || actions.length === 0)) {
|
||||
data.actionBar.clear();
|
||||
}
|
||||
});
|
||||
const actions = this.actionProvider.getActions(null, entry);
|
||||
if (data.actionBar.isEmpty() && actions && actions.length > 0) {
|
||||
data.actionBar.push(actions, { icon: true, label: false });
|
||||
} else if (!data.actionBar.isEmpty() && (!actions || actions.length === 0)) {
|
||||
data.actionBar.clear();
|
||||
}
|
||||
|
||||
// Entry group class
|
||||
if (entry instanceof QuickOpenEntryGroup && entry.getGroupLabel()) {
|
||||
@@ -622,4 +618,4 @@ export function compareEntries(elementA: QuickOpenEntry, elementB: QuickOpenEntr
|
||||
}
|
||||
|
||||
return compareAnything(nameA, nameB, lookFor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,12 +58,35 @@ export class AccessibilityProvider implements IAccessibilityProvider {
|
||||
|
||||
getPosInSet(tree: ITree, element: any): string {
|
||||
const model = this.modelProvider.getModel();
|
||||
return String(model.entries.indexOf(element) + 1);
|
||||
let i = 0;
|
||||
if (model.filter) {
|
||||
for (const entry of model.entries) {
|
||||
if (model.filter.isVisible(entry)) {
|
||||
i++;
|
||||
}
|
||||
if (entry === element) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
i = model.entries.indexOf(element) + 1;
|
||||
}
|
||||
return String(i);
|
||||
}
|
||||
|
||||
getSetSize(): string {
|
||||
const model = this.modelProvider.getModel();
|
||||
return String(model.entries.length);
|
||||
let n = 0;
|
||||
if (model.filter) {
|
||||
for (const entry of model.entries) {
|
||||
if (model.filter.isVisible(entry)) {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
n = model.entries.length;
|
||||
}
|
||||
return String(n);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,17 +2,13 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./quickopen';
|
||||
import * as nls from 'vs/nls';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import { IQuickNavigateConfiguration, IAutoFocus, IEntryRunContext, IModel, Mode, IKeyMods } from 'vs/base/parts/quickopen/common/quickOpen';
|
||||
import { Filter, Renderer, DataSource, IModelProvider, AccessibilityProvider } from 'vs/base/parts/quickopen/browser/quickOpenViewer';
|
||||
import { Builder, $ } from 'vs/base/browser/builder';
|
||||
import { ITree, ContextMenuEvent, IActionProvider, ITreeStyles, ITreeOptions, ITreeConfiguration } from 'vs/base/parts/tree/browser/tree';
|
||||
import { InputBox, MessageType, IInputBoxStyles, IRange } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
@@ -26,7 +22,7 @@ import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { StandardMouseEvent, IMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
|
||||
export interface IQuickOpenCallbacks {
|
||||
onOk: () => void;
|
||||
@@ -61,6 +57,7 @@ export interface IShowOptions {
|
||||
quickNavigateConfiguration?: IQuickNavigateConfiguration;
|
||||
autoFocus?: IAutoFocus;
|
||||
inputSelection?: IRange;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export class QuickOpenController extends DefaultController {
|
||||
@@ -72,9 +69,13 @@ export class QuickOpenController extends DefaultController {
|
||||
|
||||
return super.onContextMenu(tree, element, event);
|
||||
}
|
||||
|
||||
onMouseMiddleClick(tree: ITree, element: any, event: IMouseEvent): boolean {
|
||||
return this.onLeftClick(tree, element, event);
|
||||
}
|
||||
}
|
||||
|
||||
export enum HideReason {
|
||||
export const enum HideReason {
|
||||
ELEMENT_SELECTED,
|
||||
FOCUS_LOST,
|
||||
CANCELED
|
||||
@@ -93,18 +94,18 @@ const DEFAULT_INPUT_ARIA_LABEL = nls.localize('quickOpenAriaLabel', "Quick picke
|
||||
|
||||
export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
|
||||
private static readonly MAX_WIDTH = 600; // Max total width of quick open widget
|
||||
private static readonly MAX_WIDTH = 600; // Max total width of quick open widget
|
||||
private static readonly MAX_ITEMS_HEIGHT = 20 * 22; // Max height of item list below input field
|
||||
|
||||
private isDisposed: boolean;
|
||||
private options: IQuickOpenOptions;
|
||||
private builder: Builder;
|
||||
private element: HTMLElement;
|
||||
private tree: ITree;
|
||||
private inputBox: InputBox;
|
||||
private inputContainer: Builder;
|
||||
private helpText: Builder;
|
||||
private resultCount: Builder;
|
||||
private treeContainer: Builder;
|
||||
private inputContainer: HTMLElement;
|
||||
private helpText: HTMLElement;
|
||||
private resultCount: HTMLElement;
|
||||
private treeContainer: HTMLElement;
|
||||
private progressBar: ProgressBar;
|
||||
private visible: boolean;
|
||||
private isLoosingFocus: boolean;
|
||||
@@ -115,7 +116,7 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
private inputElement: HTMLElement;
|
||||
private layoutDimensions: DOM.Dimension;
|
||||
private model: IModel<any>;
|
||||
private inputChangingTimeoutHandle: number;
|
||||
private inputChangingTimeoutHandle: any;
|
||||
private styles: IQuickOpenStyles;
|
||||
private renderer: Renderer;
|
||||
|
||||
@@ -132,7 +133,7 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
}
|
||||
|
||||
getElement(): HTMLElement {
|
||||
return $(this.builder).getHTMLElement();
|
||||
return this.element;
|
||||
}
|
||||
|
||||
getModel(): IModel<any> {
|
||||
@@ -144,208 +145,208 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
}
|
||||
|
||||
create(): HTMLElement {
|
||||
this.builder = $().div(div => {
|
||||
|
||||
// Eventing
|
||||
div.on(DOM.EventType.KEY_DOWN, e => {
|
||||
const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e as KeyboardEvent);
|
||||
if (keyboardEvent.keyCode === KeyCode.Escape) {
|
||||
// Container
|
||||
this.element = document.createElement('div');
|
||||
DOM.addClass(this.element, 'monaco-quick-open-widget');
|
||||
this.container.appendChild(this.element);
|
||||
|
||||
this._register(DOM.addDisposableListener(this.element, DOM.EventType.CONTEXT_MENU, e => DOM.EventHelper.stop(e, true))); // Do this to fix an issue on Mac where the menu goes into the way
|
||||
this._register(DOM.addDisposableListener(this.element, DOM.EventType.FOCUS, e => this.gainingFocus(), true));
|
||||
this._register(DOM.addDisposableListener(this.element, DOM.EventType.BLUR, e => this.loosingFocus(e), true));
|
||||
this._register(DOM.addDisposableListener(this.element, DOM.EventType.KEY_DOWN, e => {
|
||||
const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
|
||||
if (keyboardEvent.keyCode === KeyCode.Escape) {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
|
||||
this.hide(HideReason.CANCELED);
|
||||
} else if (keyboardEvent.keyCode === KeyCode.Tab && !keyboardEvent.altKey && !keyboardEvent.ctrlKey && !keyboardEvent.metaKey) {
|
||||
const stops = (e.currentTarget as HTMLElement).querySelectorAll('input, .monaco-tree, .monaco-tree-row.focused .action-label.icon') as NodeListOf<HTMLElement>;
|
||||
if (keyboardEvent.shiftKey && keyboardEvent.target === stops[0]) {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
|
||||
this.hide(HideReason.CANCELED);
|
||||
} else if (keyboardEvent.keyCode === KeyCode.Tab && !keyboardEvent.altKey && !keyboardEvent.ctrlKey && !keyboardEvent.metaKey) {
|
||||
const stops = e.currentTarget.querySelectorAll('input, .monaco-tree, .monaco-tree-row.focused .action-label.icon');
|
||||
if (keyboardEvent.shiftKey && keyboardEvent.target === stops[0]) {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
stops[stops.length - 1].focus();
|
||||
} else if (!keyboardEvent.shiftKey && keyboardEvent.target === stops[stops.length - 1]) {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
stops[0].focus();
|
||||
}
|
||||
stops[stops.length - 1].focus();
|
||||
} else if (!keyboardEvent.shiftKey && keyboardEvent.target === stops[stops.length - 1]) {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
stops[0].focus();
|
||||
}
|
||||
})
|
||||
.on(DOM.EventType.CONTEXT_MENU, (e: Event) => DOM.EventHelper.stop(e, true)) // Do this to fix an issue on Mac where the menu goes into the way
|
||||
.on(DOM.EventType.FOCUS, (e: FocusEvent) => this.gainingFocus(), null, true)
|
||||
.on(DOM.EventType.BLUR, (e: FocusEvent) => this.loosingFocus(e), null, true);
|
||||
}
|
||||
}));
|
||||
|
||||
// Progress Bar
|
||||
this.progressBar = this._register(new ProgressBar(div.clone(), { progressBarBackground: this.styles.progressBarBackground }));
|
||||
this.progressBar.hide();
|
||||
// Progress Bar
|
||||
this.progressBar = this._register(new ProgressBar(this.element, { progressBarBackground: this.styles.progressBarBackground }));
|
||||
this.progressBar.hide();
|
||||
|
||||
// Input Field
|
||||
div.div({ 'class': 'quick-open-input' }, inputContainer => {
|
||||
this.inputContainer = inputContainer;
|
||||
this.inputBox = this._register(new InputBox(inputContainer.getHTMLElement(), null, {
|
||||
placeholder: this.options.inputPlaceHolder || '',
|
||||
ariaLabel: DEFAULT_INPUT_ARIA_LABEL,
|
||||
inputBackground: this.styles.inputBackground,
|
||||
inputForeground: this.styles.inputForeground,
|
||||
inputBorder: this.styles.inputBorder,
|
||||
inputValidationInfoBackground: this.styles.inputValidationInfoBackground,
|
||||
inputValidationInfoBorder: this.styles.inputValidationInfoBorder,
|
||||
inputValidationWarningBackground: this.styles.inputValidationWarningBackground,
|
||||
inputValidationWarningBorder: this.styles.inputValidationWarningBorder,
|
||||
inputValidationErrorBackground: this.styles.inputValidationErrorBackground,
|
||||
inputValidationErrorBorder: this.styles.inputValidationErrorBorder
|
||||
}));
|
||||
// Input Field
|
||||
this.inputContainer = document.createElement('div');
|
||||
DOM.addClass(this.inputContainer, 'quick-open-input');
|
||||
this.element.appendChild(this.inputContainer);
|
||||
|
||||
// ARIA
|
||||
this.inputElement = this.inputBox.inputElement;
|
||||
this.inputElement.setAttribute('role', 'combobox');
|
||||
this.inputElement.setAttribute('aria-haspopup', 'false');
|
||||
this.inputElement.setAttribute('aria-autocomplete', 'list');
|
||||
this.inputBox = this._register(new InputBox(this.inputContainer, null, {
|
||||
placeholder: this.options.inputPlaceHolder || '',
|
||||
ariaLabel: DEFAULT_INPUT_ARIA_LABEL,
|
||||
inputBackground: this.styles.inputBackground,
|
||||
inputForeground: this.styles.inputForeground,
|
||||
inputBorder: this.styles.inputBorder,
|
||||
inputValidationInfoBackground: this.styles.inputValidationInfoBackground,
|
||||
inputValidationInfoForeground: this.styles.inputValidationInfoForeground,
|
||||
inputValidationInfoBorder: this.styles.inputValidationInfoBorder,
|
||||
inputValidationWarningBackground: this.styles.inputValidationWarningBackground,
|
||||
inputValidationWarningForeground: this.styles.inputValidationWarningForeground,
|
||||
inputValidationWarningBorder: this.styles.inputValidationWarningBorder,
|
||||
inputValidationErrorBackground: this.styles.inputValidationErrorBackground,
|
||||
inputValidationErrorForeground: this.styles.inputValidationErrorForeground,
|
||||
inputValidationErrorBorder: this.styles.inputValidationErrorBorder
|
||||
}));
|
||||
|
||||
DOM.addDisposableListener(this.inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
|
||||
const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
|
||||
const shouldOpenInBackground = this.shouldOpenInBackground(keyboardEvent);
|
||||
this.inputElement = this.inputBox.inputElement;
|
||||
this.inputElement.setAttribute('role', 'combobox');
|
||||
this.inputElement.setAttribute('aria-haspopup', 'false');
|
||||
this.inputElement.setAttribute('aria-autocomplete', 'list');
|
||||
|
||||
// Do not handle Tab: It is used to navigate between elements without mouse
|
||||
if (keyboardEvent.keyCode === KeyCode.Tab) {
|
||||
return;
|
||||
this._register(DOM.addDisposableListener(this.inputBox.inputElement, DOM.EventType.INPUT, (e: Event) => this.onType()));
|
||||
this._register(DOM.addDisposableListener(this.inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
|
||||
const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
|
||||
const shouldOpenInBackground = this.shouldOpenInBackground(keyboardEvent);
|
||||
|
||||
// Do not handle Tab: It is used to navigate between elements without mouse
|
||||
if (keyboardEvent.keyCode === KeyCode.Tab) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass tree navigation keys to the tree but leave focus in input field
|
||||
else if (keyboardEvent.keyCode === KeyCode.DownArrow || keyboardEvent.keyCode === KeyCode.UpArrow || keyboardEvent.keyCode === KeyCode.PageDown || keyboardEvent.keyCode === KeyCode.PageUp) {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
|
||||
this.navigateInTree(keyboardEvent.keyCode, keyboardEvent.shiftKey);
|
||||
|
||||
// Position cursor at the end of input to allow right arrow (open in background)
|
||||
// to function immediately unless the user has made a selection
|
||||
if (this.inputBox.inputElement.selectionStart === this.inputBox.inputElement.selectionEnd) {
|
||||
this.inputBox.inputElement.selectionStart = this.inputBox.value.length;
|
||||
}
|
||||
}
|
||||
|
||||
// Select element on Enter or on Arrow-Right if we are at the end of the input
|
||||
else if (keyboardEvent.keyCode === KeyCode.Enter || shouldOpenInBackground) {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
|
||||
const focus = this.tree.getFocus();
|
||||
if (focus) {
|
||||
this.elementSelected(focus, e, shouldOpenInBackground ? Mode.OPEN_IN_BACKGROUND : Mode.OPEN);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Result count for screen readers
|
||||
this.resultCount = document.createElement('div');
|
||||
DOM.addClass(this.resultCount, 'quick-open-result-count');
|
||||
this.resultCount.setAttribute('aria-live', 'polite');
|
||||
this.resultCount.setAttribute('aria-atomic', 'true');
|
||||
this.element.appendChild(this.resultCount);
|
||||
|
||||
// Tree
|
||||
this.treeContainer = document.createElement('div');
|
||||
DOM.addClass(this.treeContainer, 'quick-open-tree');
|
||||
this.element.appendChild(this.treeContainer);
|
||||
|
||||
const createTree = this.options.treeCreator || ((container, config, opts) => new Tree(container, config, opts));
|
||||
|
||||
this.tree = this._register(createTree(this.treeContainer, {
|
||||
dataSource: new DataSource(this),
|
||||
controller: new QuickOpenController({ clickBehavior: ClickBehavior.ON_MOUSE_UP, keyboardSupport: this.options.keyboardSupport }),
|
||||
renderer: (this.renderer = new Renderer(this, this.styles)),
|
||||
filter: new Filter(this),
|
||||
accessibilityProvider: new AccessibilityProvider(this)
|
||||
}, {
|
||||
twistiePixels: 11,
|
||||
indentPixels: 0,
|
||||
alwaysFocused: true,
|
||||
verticalScrollMode: ScrollbarVisibility.Visible,
|
||||
horizontalScrollMode: ScrollbarVisibility.Hidden,
|
||||
ariaLabel: nls.localize('treeAriaLabel', "Quick Picker"),
|
||||
keyboardSupport: this.options.keyboardSupport,
|
||||
preventRootFocus: false
|
||||
}));
|
||||
|
||||
this.treeElement = this.tree.getHTMLElement();
|
||||
|
||||
// Handle Focus and Selection event
|
||||
this._register(this.tree.onDidChangeFocus(event => {
|
||||
this.elementFocused(event.focus, event);
|
||||
}));
|
||||
|
||||
this._register(this.tree.onDidChangeSelection(event => {
|
||||
if (event.selection && event.selection.length > 0) {
|
||||
const mouseEvent: StandardMouseEvent = event.payload && event.payload.originalEvent instanceof StandardMouseEvent ? event.payload.originalEvent : void 0;
|
||||
const shouldOpenInBackground = mouseEvent ? this.shouldOpenInBackground(mouseEvent) : false;
|
||||
|
||||
this.elementSelected(event.selection[0], event, shouldOpenInBackground ? Mode.OPEN_IN_BACKGROUND : Mode.OPEN);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(this.treeContainer, DOM.EventType.KEY_DOWN, e => {
|
||||
const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
|
||||
|
||||
// Only handle when in quick navigation mode
|
||||
if (!this.quickNavigateConfiguration) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Support keyboard navigation in quick navigation mode
|
||||
if (keyboardEvent.keyCode === KeyCode.DownArrow || keyboardEvent.keyCode === KeyCode.UpArrow || keyboardEvent.keyCode === KeyCode.PageDown || keyboardEvent.keyCode === KeyCode.PageUp) {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
|
||||
this.navigateInTree(keyboardEvent.keyCode);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(this.treeContainer, DOM.EventType.KEY_UP, e => {
|
||||
const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
|
||||
const keyCode = keyboardEvent.keyCode;
|
||||
|
||||
// Only handle when in quick navigation mode
|
||||
if (!this.quickNavigateConfiguration) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Select element when keys are pressed that signal it
|
||||
const quickNavKeys = this.quickNavigateConfiguration.keybindings;
|
||||
const wasTriggerKeyPressed = keyCode === KeyCode.Enter || quickNavKeys.some(k => {
|
||||
const [firstPart, chordPart] = k.getParts();
|
||||
if (chordPart) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (firstPart.shiftKey && keyCode === KeyCode.Shift) {
|
||||
if (keyboardEvent.ctrlKey || keyboardEvent.altKey || keyboardEvent.metaKey) {
|
||||
return false; // this is an optimistic check for the shift key being used to navigate back in quick open
|
||||
}
|
||||
|
||||
// Pass tree navigation keys to the tree but leave focus in input field
|
||||
else if (keyboardEvent.keyCode === KeyCode.DownArrow || keyboardEvent.keyCode === KeyCode.UpArrow || keyboardEvent.keyCode === KeyCode.PageDown || keyboardEvent.keyCode === KeyCode.PageUp) {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
this.navigateInTree(keyboardEvent.keyCode, keyboardEvent.shiftKey);
|
||||
if (firstPart.altKey && keyCode === KeyCode.Alt) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Position cursor at the end of input to allow right arrow (open in background)
|
||||
// to function immediately unless the user has made a selection
|
||||
if (this.inputBox.inputElement.selectionStart === this.inputBox.inputElement.selectionEnd) {
|
||||
this.inputBox.inputElement.selectionStart = this.inputBox.value.length;
|
||||
}
|
||||
}
|
||||
if (firstPart.ctrlKey && keyCode === KeyCode.Ctrl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Select element on Enter or on Arrow-Right if we are at the end of the input
|
||||
else if (keyboardEvent.keyCode === KeyCode.Enter || shouldOpenInBackground) {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
if (firstPart.metaKey && keyCode === KeyCode.Meta) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const focus = this.tree.getFocus();
|
||||
if (focus) {
|
||||
this.elementSelected(focus, e, shouldOpenInBackground ? Mode.OPEN_IN_BACKGROUND : Mode.OPEN);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
DOM.addDisposableListener(this.inputBox.inputElement, DOM.EventType.INPUT, (e: Event) => {
|
||||
this.onType();
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
// Result count for screen readers
|
||||
this.resultCount = div.div({
|
||||
'class': 'quick-open-result-count',
|
||||
'aria-live': 'polite'
|
||||
}).clone();
|
||||
|
||||
// Tree
|
||||
this.treeContainer = div.div({
|
||||
'class': 'quick-open-tree'
|
||||
}, div => {
|
||||
const createTree = this.options.treeCreator || ((container, config, opts) => new Tree(container, config, opts));
|
||||
|
||||
this.tree = this._register(createTree(div.getHTMLElement(), {
|
||||
dataSource: new DataSource(this),
|
||||
controller: new QuickOpenController({ clickBehavior: ClickBehavior.ON_MOUSE_UP, keyboardSupport: this.options.keyboardSupport }),
|
||||
renderer: (this.renderer = new Renderer(this, this.styles)),
|
||||
filter: new Filter(this),
|
||||
accessibilityProvider: new AccessibilityProvider(this)
|
||||
}, {
|
||||
twistiePixels: 11,
|
||||
indentPixels: 0,
|
||||
alwaysFocused: true,
|
||||
verticalScrollMode: ScrollbarVisibility.Visible,
|
||||
horizontalScrollMode: ScrollbarVisibility.Hidden,
|
||||
ariaLabel: nls.localize('treeAriaLabel', "Quick Picker"),
|
||||
keyboardSupport: this.options.keyboardSupport,
|
||||
preventRootFocus: false
|
||||
}));
|
||||
|
||||
this.treeElement = this.tree.getHTMLElement();
|
||||
|
||||
// Handle Focus and Selection event
|
||||
this._register(this.tree.onDidChangeFocus(event => {
|
||||
this.elementFocused(event.focus, event);
|
||||
}));
|
||||
|
||||
this._register(this.tree.onDidChangeSelection(event => {
|
||||
if (event.selection && event.selection.length > 0) {
|
||||
const mouseEvent: StandardMouseEvent = event.payload && event.payload.originalEvent instanceof StandardMouseEvent ? event.payload.originalEvent : void 0;
|
||||
const shouldOpenInBackground = mouseEvent ? this.shouldOpenInBackground(mouseEvent) : false;
|
||||
|
||||
this.elementSelected(event.selection[0], event, shouldOpenInBackground ? Mode.OPEN_IN_BACKGROUND : Mode.OPEN);
|
||||
}
|
||||
}));
|
||||
}).
|
||||
on(DOM.EventType.KEY_DOWN, e => {
|
||||
const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e as KeyboardEvent);
|
||||
|
||||
// Only handle when in quick navigation mode
|
||||
if (!this.quickNavigateConfiguration) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Support keyboard navigation in quick navigation mode
|
||||
if (keyboardEvent.keyCode === KeyCode.DownArrow || keyboardEvent.keyCode === KeyCode.UpArrow || keyboardEvent.keyCode === KeyCode.PageDown || keyboardEvent.keyCode === KeyCode.PageUp) {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
|
||||
this.navigateInTree(keyboardEvent.keyCode);
|
||||
}
|
||||
}).
|
||||
on(DOM.EventType.KEY_UP, e => {
|
||||
const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e as KeyboardEvent);
|
||||
const keyCode = keyboardEvent.keyCode;
|
||||
|
||||
// Only handle when in quick navigation mode
|
||||
if (!this.quickNavigateConfiguration) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Select element when keys are pressed that signal it
|
||||
const quickNavKeys = this.quickNavigateConfiguration.keybindings;
|
||||
const wasTriggerKeyPressed = keyCode === KeyCode.Enter || quickNavKeys.some(k => {
|
||||
const [firstPart, chordPart] = k.getParts();
|
||||
if (chordPart) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (firstPart.shiftKey && keyCode === KeyCode.Shift) {
|
||||
if (keyboardEvent.ctrlKey || keyboardEvent.altKey || keyboardEvent.metaKey) {
|
||||
return false; // this is an optimistic check for the shift key being used to navigate back in quick open
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (firstPart.altKey && keyCode === KeyCode.Alt) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (firstPart.ctrlKey && keyCode === KeyCode.Ctrl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (firstPart.metaKey && keyCode === KeyCode.Meta) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (wasTriggerKeyPressed) {
|
||||
const focus = this.tree.getFocus();
|
||||
if (focus) {
|
||||
this.elementSelected(focus, e);
|
||||
}
|
||||
}
|
||||
}).
|
||||
clone();
|
||||
})
|
||||
|
||||
// Widget Attributes
|
||||
.addClass('monaco-quick-open-widget')
|
||||
.build(this.container);
|
||||
if (wasTriggerKeyPressed) {
|
||||
const focus = this.tree.getFocus();
|
||||
if (focus) {
|
||||
this.elementSelected(focus, e);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Support layout
|
||||
if (this.layoutDimensions) {
|
||||
@@ -355,7 +356,7 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
this.applyStyles();
|
||||
|
||||
// Allows focus to switch to next/previous entry after tab into an actionbar item
|
||||
DOM.addDisposableListener(this.treeContainer.getHTMLElement(), DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
|
||||
this._register(DOM.addDisposableListener(this.treeContainer, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
|
||||
const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
|
||||
// Only handle when not in quick navigation mode
|
||||
if (this.quickNavigateConfiguration) {
|
||||
@@ -366,8 +367,9 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
this.navigateInTree(keyboardEvent.keyCode, keyboardEvent.shiftKey);
|
||||
this.treeElement.focus();
|
||||
}
|
||||
});
|
||||
return this.builder.getHTMLElement();
|
||||
}));
|
||||
|
||||
return this.element;
|
||||
}
|
||||
|
||||
style(styles: IQuickOpenStyles): void {
|
||||
@@ -377,18 +379,18 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
}
|
||||
|
||||
protected applyStyles(): void {
|
||||
if (this.builder) {
|
||||
if (this.element) {
|
||||
const foreground = this.styles.foreground ? this.styles.foreground.toString() : null;
|
||||
const background = this.styles.background ? this.styles.background.toString() : null;
|
||||
const borderColor = this.styles.borderColor ? this.styles.borderColor.toString() : null;
|
||||
const widgetShadow = this.styles.widgetShadow ? this.styles.widgetShadow.toString() : null;
|
||||
|
||||
this.builder.style('color', foreground);
|
||||
this.builder.style('background-color', background);
|
||||
this.builder.style('border-color', borderColor);
|
||||
this.builder.style('border-width', borderColor ? '1px' : null);
|
||||
this.builder.style('border-style', borderColor ? 'solid' : null);
|
||||
this.builder.style('box-shadow', widgetShadow ? `0 5px 8px ${widgetShadow}` : null);
|
||||
this.element.style.color = foreground;
|
||||
this.element.style.backgroundColor = background;
|
||||
this.element.style.borderColor = borderColor;
|
||||
this.element.style.borderWidth = borderColor ? '1px' : null;
|
||||
this.element.style.borderStyle = borderColor ? 'solid' : null;
|
||||
this.element.style.boxShadow = widgetShadow ? `0 5px 8px ${widgetShadow}` : null;
|
||||
}
|
||||
|
||||
if (this.progressBar) {
|
||||
@@ -403,10 +405,13 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
inputForeground: this.styles.inputForeground,
|
||||
inputBorder: this.styles.inputBorder,
|
||||
inputValidationInfoBackground: this.styles.inputValidationInfoBackground,
|
||||
inputValidationInfoForeground: this.styles.inputValidationInfoForeground,
|
||||
inputValidationInfoBorder: this.styles.inputValidationInfoBorder,
|
||||
inputValidationWarningBackground: this.styles.inputValidationWarningBackground,
|
||||
inputValidationWarningForeground: this.styles.inputValidationWarningForeground,
|
||||
inputValidationWarningBorder: this.styles.inputValidationWarningBorder,
|
||||
inputValidationErrorBackground: this.styles.inputValidationErrorBackground,
|
||||
inputValidationErrorForeground: this.styles.inputValidationErrorForeground,
|
||||
inputValidationErrorBorder: this.styles.inputValidationErrorBorder
|
||||
});
|
||||
}
|
||||
@@ -448,9 +453,9 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
// Adjust help text as needed if present
|
||||
if (this.helpText) {
|
||||
if (value) {
|
||||
this.helpText.hide();
|
||||
DOM.hide(this.helpText);
|
||||
} else {
|
||||
this.helpText.show();
|
||||
DOM.show(this.helpText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,7 +528,7 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
// Reveal
|
||||
newFocus = this.tree.getFocus();
|
||||
if (newFocus) {
|
||||
this.tree.reveal(newFocus).done(null, errors.onUnexpectedError);
|
||||
this.tree.reveal(newFocus);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,24 +578,24 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
|
||||
// Adjust UI for quick navigate mode
|
||||
if (this.quickNavigateConfiguration) {
|
||||
this.inputContainer.hide();
|
||||
this.builder.show();
|
||||
DOM.hide(this.inputContainer);
|
||||
DOM.show(this.element);
|
||||
this.tree.domFocus();
|
||||
}
|
||||
|
||||
// Otherwise use normal UI
|
||||
else {
|
||||
this.inputContainer.show();
|
||||
this.builder.show();
|
||||
DOM.show(this.inputContainer);
|
||||
DOM.show(this.element);
|
||||
this.inputBox.focus();
|
||||
}
|
||||
|
||||
// Adjust Help text for IE
|
||||
if (this.helpText) {
|
||||
if (this.quickNavigateConfiguration || types.isString(param)) {
|
||||
this.helpText.hide();
|
||||
DOM.hide(this.helpText);
|
||||
} else {
|
||||
this.helpText.show();
|
||||
DOM.show(this.helpText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,6 +603,9 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
if (types.isString(param)) {
|
||||
this.doShowWithPrefix(param);
|
||||
} else {
|
||||
if (options.value) {
|
||||
this.restoreLastInput(options.value);
|
||||
}
|
||||
this.doShowWithInput(param, options && options.autoFocus ? options.autoFocus : {});
|
||||
}
|
||||
|
||||
@@ -611,6 +619,12 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private restoreLastInput(lastInput: string) {
|
||||
this.inputBox.value = lastInput;
|
||||
this.inputBox.select();
|
||||
this.callbacks.onType(lastInput);
|
||||
}
|
||||
|
||||
private doShowWithPrefix(prefix: string): void {
|
||||
this.inputBox.value = prefix;
|
||||
this.callbacks.onType(prefix);
|
||||
@@ -621,7 +635,7 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
}
|
||||
|
||||
private setInputAndLayout(input: IModel<any>, autoFocus: IAutoFocus): void {
|
||||
this.treeContainer.style({ height: `${this.getHeight(input)}px` });
|
||||
this.treeContainer.style.height = `${this.getHeight(input)}px`;
|
||||
|
||||
this.tree.setInput(null).then(() => {
|
||||
this.model = input;
|
||||
@@ -630,7 +644,7 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
this.inputElement.setAttribute('aria-haspopup', String(input && input.entries && input.entries.length > 0));
|
||||
|
||||
return this.tree.setInput(input);
|
||||
}).done(() => {
|
||||
}).then(() => {
|
||||
|
||||
// Indicate entries to tree
|
||||
this.tree.layout();
|
||||
@@ -642,7 +656,7 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
if (entries.length) {
|
||||
this.autoFocus(input, entries, autoFocus);
|
||||
}
|
||||
}, errors.onUnexpectedError);
|
||||
});
|
||||
}
|
||||
|
||||
private isElementVisible<T>(input: IModel<T>, e: T): boolean {
|
||||
@@ -679,7 +693,7 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
const entryToFocus = caseSensitiveMatch || caseInsensitiveMatch;
|
||||
if (entryToFocus) {
|
||||
this.tree.setFocus(entryToFocus);
|
||||
this.tree.reveal(entryToFocus, 0.5).done(null, errors.onUnexpectedError);
|
||||
this.tree.reveal(entryToFocus, 0.5);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -688,14 +702,14 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
// Second check for auto focus of first entry
|
||||
if (autoFocus.autoFocusFirstEntry) {
|
||||
this.tree.focusFirst();
|
||||
this.tree.reveal(this.tree.getFocus()).done(null, errors.onUnexpectedError);
|
||||
this.tree.reveal(this.tree.getFocus());
|
||||
}
|
||||
|
||||
// Third check for specific index option
|
||||
else if (typeof autoFocus.autoFocusIndex === 'number') {
|
||||
if (entries.length > autoFocus.autoFocusIndex) {
|
||||
this.tree.focusNth(autoFocus.autoFocusIndex);
|
||||
this.tree.reveal(this.tree.getFocus()).done(null, errors.onUnexpectedError);
|
||||
this.tree.reveal(this.tree.getFocus());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -728,8 +742,8 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
}
|
||||
|
||||
// Apply height & Refresh
|
||||
this.treeContainer.style({ height: `${this.getHeight(input)}px` });
|
||||
this.tree.refresh().done(() => {
|
||||
this.treeContainer.style.height = `${this.getHeight(input)}px`;
|
||||
this.tree.refresh().then(() => {
|
||||
|
||||
// Indicate entries to tree
|
||||
this.tree.layout();
|
||||
@@ -743,7 +757,7 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
this.autoFocus(input, entries, autoFocus);
|
||||
}
|
||||
}
|
||||
}, errors.onUnexpectedError);
|
||||
});
|
||||
}
|
||||
|
||||
private getHeight(input: IModel<any>): number {
|
||||
@@ -781,7 +795,7 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
}
|
||||
|
||||
updateResultCount(count: number) {
|
||||
this.resultCount.text(nls.localize({ key: 'quickInput.visibleCount', comment: ['This tells the user how many items are shown in a list of items to select from. The items can be anything. Currently not visible, but read by screen readers.'] }, "{0} Results", count));
|
||||
this.resultCount.textContent = nls.localize({ key: 'quickInput.visibleCount', comment: ['This tells the user how many items are shown in a list of items to select from. The items can be anything. Currently not visible, but read by screen readers.'] }, "{0} Results", count);
|
||||
}
|
||||
|
||||
hide(reason?: HideReason): void {
|
||||
@@ -790,8 +804,8 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
}
|
||||
|
||||
this.visible = false;
|
||||
this.builder.hide();
|
||||
this.builder.domBlur();
|
||||
DOM.hide(this.element);
|
||||
this.element.blur();
|
||||
|
||||
// Clear input field and clear tree
|
||||
this.inputBox.value = '';
|
||||
@@ -801,7 +815,7 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
this.inputElement.setAttribute('aria-haspopup', 'false');
|
||||
|
||||
// Reset Tree Height
|
||||
this.treeContainer.style({ height: (this.options.minItemsToShow ? this.options.minItemsToShow * 22 : 0) + 'px' });
|
||||
this.treeContainer.style.height = `${this.options.minItemsToShow ? this.options.minItemsToShow * 22 : 0}px`;
|
||||
|
||||
// Clear any running Progress
|
||||
this.progressBar.stop().hide();
|
||||
@@ -882,9 +896,9 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
|
||||
// when the input is changing in quick open, we indicate this as CSS class to the widget
|
||||
// for a certain timeout. this helps reducing some hectic UI updates when input changes quickly
|
||||
this.builder.addClass('content-changing');
|
||||
DOM.addClass(this.element, 'content-changing');
|
||||
this.inputChangingTimeoutHandle = setTimeout(() => {
|
||||
this.builder.removeClass('content-changing');
|
||||
DOM.removeClass(this.element, 'content-changing');
|
||||
}, 500);
|
||||
}
|
||||
|
||||
@@ -928,16 +942,16 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
}
|
||||
|
||||
setExtraClass(clazz: string): void {
|
||||
const previousClass = this.builder.getProperty('extra-class');
|
||||
const previousClass = this.element.getAttribute('quick-open-extra-class');
|
||||
if (previousClass) {
|
||||
this.builder.removeClass(previousClass);
|
||||
DOM.removeClasses(this.element, previousClass);
|
||||
}
|
||||
|
||||
if (clazz) {
|
||||
this.builder.addClass(clazz);
|
||||
this.builder.setProperty('extra-class', clazz);
|
||||
DOM.addClasses(this.element, clazz);
|
||||
this.element.setAttribute('quick-open-extra-class', clazz);
|
||||
} else if (previousClass) {
|
||||
this.builder.removeProperty('extra-class');
|
||||
this.element.removeAttribute('quick-open-extra-class');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -950,18 +964,14 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
|
||||
// Apply to quick open width (height is dynamic by number of items to show)
|
||||
const quickOpenWidth = Math.min(this.layoutDimensions.width * 0.62 /* golden cut */, QuickOpenWidget.MAX_WIDTH);
|
||||
if (this.builder) {
|
||||
if (this.element) {
|
||||
|
||||
// quick open
|
||||
this.builder.style({
|
||||
width: quickOpenWidth + 'px',
|
||||
marginLeft: '-' + (quickOpenWidth / 2) + 'px'
|
||||
});
|
||||
this.element.style.width = `${quickOpenWidth}px`;
|
||||
this.element.style.marginLeft = `-${quickOpenWidth / 2}px`;
|
||||
|
||||
// input field
|
||||
this.inputContainer.style({
|
||||
width: (quickOpenWidth - 12) + 'px'
|
||||
});
|
||||
this.inputContainer.style.width = `${quickOpenWidth - 12}px`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -975,16 +985,13 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
}
|
||||
|
||||
const relatedTarget = e.relatedTarget as HTMLElement;
|
||||
if (!this.quickNavigateConfiguration && DOM.isAncestor(relatedTarget, this.builder.getHTMLElement())) {
|
||||
if (!this.quickNavigateConfiguration && DOM.isAncestor(relatedTarget, this.element)) {
|
||||
return; // user clicked somewhere into quick open widget, do not close thereby
|
||||
}
|
||||
|
||||
this.isLoosingFocus = true;
|
||||
TPromise.timeout(0).then(() => {
|
||||
if (!this.isLoosingFocus) {
|
||||
return;
|
||||
}
|
||||
if (this.isDisposed) {
|
||||
setTimeout(() => {
|
||||
if (!this.isLoosingFocus || this.isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -992,7 +999,7 @@ export class QuickOpenWidget extends Disposable implements IModelProvider {
|
||||
if (!veto) {
|
||||
this.hide(HideReason.FOCUS_LOST);
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
|
||||
@@ -41,7 +40,7 @@ export interface IAutoFocus {
|
||||
autoFocusPrefixMatch?: string;
|
||||
}
|
||||
|
||||
export enum Mode {
|
||||
export const enum Mode {
|
||||
PREVIEW,
|
||||
OPEN,
|
||||
OPEN_IN_BACKGROUND
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { compareAnything } from 'vs/base/common/comparers';
|
||||
import { matchesPrefix, IMatch, createMatches, matchesCamelCase, isUpper } from 'vs/base/common/filters';
|
||||
import { nativeSep } from 'vs/base/common/paths';
|
||||
@@ -63,8 +61,8 @@ export function score(target: string, query: string, queryLower: string, fuzzy:
|
||||
}
|
||||
|
||||
function doScore(query: string, queryLower: string, queryLength: number, target: string, targetLower: string, targetLength: number): [number, number[]] {
|
||||
const scores = [];
|
||||
const matches = [];
|
||||
const scores: number[] = [];
|
||||
const matches: number[] = [];
|
||||
|
||||
//
|
||||
// Build Scorer Matrix:
|
||||
@@ -123,7 +121,7 @@ function doScore(query: string, queryLower: string, queryLength: number, target:
|
||||
}
|
||||
|
||||
// Restore Positions (starting from bottom right of matrix)
|
||||
const positions = [];
|
||||
const positions: number[] = [];
|
||||
let queryIndex = queryLength - 1;
|
||||
let targetIndex = targetLength - 1;
|
||||
while (queryIndex >= 0 && targetIndex >= 0) {
|
||||
@@ -306,20 +304,18 @@ export interface IPreparedQuery {
|
||||
* Helper function to prepare a search value for scoring in quick open by removing unwanted characters.
|
||||
*/
|
||||
export function prepareQuery(original: string): IPreparedQuery {
|
||||
let lowercase: string;
|
||||
let containsPathSeparator: boolean;
|
||||
let value: string;
|
||||
|
||||
if (original) {
|
||||
value = stripWildcards(original).replace(/\s/g, ''); // get rid of all wildcards and whitespace
|
||||
if (isWindows) {
|
||||
value = value.replace(/\//g, nativeSep); // Help Windows users to search for paths when using slash
|
||||
}
|
||||
|
||||
lowercase = value.toLowerCase();
|
||||
containsPathSeparator = value.indexOf(nativeSep) >= 0;
|
||||
if (!original) {
|
||||
original = '';
|
||||
}
|
||||
|
||||
let value = stripWildcards(original).replace(/\s/g, ''); // get rid of all wildcards and whitespace
|
||||
if (isWindows) {
|
||||
value = value.replace(/\//g, nativeSep); // Help Windows users to search for paths when using slash
|
||||
}
|
||||
|
||||
const lowercase = value.toLowerCase();
|
||||
const containsPathSeparator = value.indexOf(nativeSep) >= 0;
|
||||
|
||||
return { original, value, lowercase, containsPathSeparator };
|
||||
}
|
||||
|
||||
@@ -505,28 +501,25 @@ export function compareItemsByScore<T>(itemA: T, itemB: T, query: IPreparedQuery
|
||||
}
|
||||
|
||||
function computeLabelAndDescriptionMatchDistance<T>(item: T, score: IItemScore, accessor: IItemAccessor<T>): number {
|
||||
const hasLabelMatches = (score.labelMatch && score.labelMatch.length);
|
||||
const hasDescriptionMatches = (score.descriptionMatch && score.descriptionMatch.length);
|
||||
|
||||
let matchStart: number = -1;
|
||||
let matchEnd: number = -1;
|
||||
|
||||
// If we have description matches, the start is first of description match
|
||||
if (hasDescriptionMatches) {
|
||||
if (score.descriptionMatch && score.descriptionMatch.length) {
|
||||
matchStart = score.descriptionMatch[0].start;
|
||||
}
|
||||
|
||||
// Otherwise, the start is the first label match
|
||||
else if (hasLabelMatches) {
|
||||
else if (score.labelMatch && score.labelMatch.length) {
|
||||
matchStart = score.labelMatch[0].start;
|
||||
}
|
||||
|
||||
// If we have label match, the end is the last label match
|
||||
// If we had a description match, we add the length of the description
|
||||
// as offset to the end to indicate this.
|
||||
if (hasLabelMatches) {
|
||||
if (score.labelMatch && score.labelMatch.length) {
|
||||
matchEnd = score.labelMatch[score.labelMatch.length - 1].end;
|
||||
if (hasDescriptionMatches) {
|
||||
if (score.descriptionMatch && score.descriptionMatch.length) {
|
||||
const itemDescription = accessor.getItemDescription(item);
|
||||
if (itemDescription) {
|
||||
matchEnd += itemDescription.length;
|
||||
@@ -535,7 +528,7 @@ function computeLabelAndDescriptionMatchDistance<T>(item: T, score: IItemScore,
|
||||
}
|
||||
|
||||
// If we have just a description match, the end is the last description match
|
||||
else if (hasDescriptionMatches) {
|
||||
else if (score.descriptionMatch && score.descriptionMatch.length) {
|
||||
matchEnd = score.descriptionMatch[score.descriptionMatch.length - 1].end;
|
||||
}
|
||||
|
||||
@@ -543,7 +536,7 @@ function computeLabelAndDescriptionMatchDistance<T>(item: T, score: IItemScore,
|
||||
}
|
||||
|
||||
function compareByMatchLength(matchesA?: IMatch[], matchesB?: IMatch[]): number {
|
||||
if ((!matchesA && !matchesB) || (!matchesA.length && !matchesB.length)) {
|
||||
if ((!matchesA && !matchesB) || ((!matchesA || !matchesA.length) && (!matchesB || !matchesB.length))) {
|
||||
return 0; // make sure to not cause bad comparing when matches are not provided
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { QuickOpenModel, QuickOpenEntry, QuickOpenEntryGroup } from 'vs/base/parts/quickopen/browser/quickOpenModel';
|
||||
import { DataSource } from 'vs/base/parts/quickopen/browser/quickOpenViewer';
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as scorer from 'vs/base/parts/quickopen/common/quickOpenScorer';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { basename, dirname, nativeSep } from 'vs/base/common/paths';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
|
||||
@@ -812,7 +810,7 @@ suite('Quick Open Scorer', () => {
|
||||
assert.equal(res[0], resourceB);
|
||||
});
|
||||
|
||||
test('prepareSearchForScoring', function () {
|
||||
test('prepareSearchForScoring', () => {
|
||||
assert.equal(scorer.prepareQuery(' f*a ').value, 'fa');
|
||||
assert.equal(scorer.prepareQuery('model Tester.ts').value, 'modelTester.ts');
|
||||
assert.equal(scorer.prepareQuery('Model Tester.ts').lowercase, 'modeltester.ts');
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as WinJS from 'vs/base/common/winjs.base';
|
||||
import * as Touch from 'vs/base/browser/touch';
|
||||
@@ -364,6 +363,10 @@ export interface IDataSource {
|
||||
/**
|
||||
* Returns the unique identifier of the given element.
|
||||
* No more than one element may use a given identifier.
|
||||
*
|
||||
* You should not attempt to "move" an element to a different
|
||||
* parent by keeping its ID. The idea here is to have tree location
|
||||
* related IDs (eg. full file path, in the Explorer example).
|
||||
*/
|
||||
getId(tree: ITree, element: any): string;
|
||||
|
||||
@@ -551,6 +554,11 @@ export interface IController {
|
||||
*/
|
||||
onKeyUp(tree: ITree, event: Keyboard.IKeyboardEvent): boolean;
|
||||
|
||||
/**
|
||||
* Called when a mouse middle button is pressed down on an element.
|
||||
*/
|
||||
onMouseMiddleClick?(tree: ITree, element: any, event: Mouse.IMouseEvent): boolean;
|
||||
|
||||
/**
|
||||
* Called when a mouse button is pressed down on an element.
|
||||
*/
|
||||
@@ -562,12 +570,12 @@ export interface IController {
|
||||
onMouseUp?(tree: ITree, element: any, event: Mouse.IMouseEvent): boolean;
|
||||
}
|
||||
|
||||
export enum DragOverEffect {
|
||||
export const enum DragOverEffect {
|
||||
COPY,
|
||||
MOVE
|
||||
}
|
||||
|
||||
export enum DragOverBubble {
|
||||
export const enum DragOverBubble {
|
||||
BUBBLE_DOWN,
|
||||
BUBBLE_UP
|
||||
}
|
||||
@@ -731,7 +739,7 @@ export interface IActionProvider {
|
||||
/**
|
||||
* Returns a promise of an array with the actions of the element that should show up in place right to the element in the tree.
|
||||
*/
|
||||
getActions(tree: ITree, element: any): WinJS.TPromise<IAction[]>;
|
||||
getActions(tree: ITree, element: any): IAction[];
|
||||
|
||||
/**
|
||||
* Returns whether or not the element has secondary actions. These show up once the user has expanded the element's action bar.
|
||||
@@ -741,10 +749,10 @@ export interface IActionProvider {
|
||||
/**
|
||||
* Returns a promise of an array with the secondary actions of the element that should show up once the user has expanded the element's action bar.
|
||||
*/
|
||||
getSecondaryActions(tree: ITree, element: any): WinJS.TPromise<IAction[]>;
|
||||
getSecondaryActions(tree: ITree, element: any): IAction[];
|
||||
|
||||
/**
|
||||
* Returns an action item to render an action.
|
||||
*/
|
||||
getActionItem(tree: ITree, element: any, action: IAction): IActionItem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import * as touch from 'vs/base/browser/touch';
|
||||
@@ -25,7 +23,7 @@ export interface ICancelableEvent {
|
||||
stopPropagation(): void;
|
||||
}
|
||||
|
||||
export enum ClickBehavior {
|
||||
export const enum ClickBehavior {
|
||||
|
||||
/**
|
||||
* Handle the click when the mouse button is pressed but not released yet.
|
||||
@@ -38,7 +36,7 @@ export enum ClickBehavior {
|
||||
ON_MOUSE_UP
|
||||
}
|
||||
|
||||
export enum OpenMode {
|
||||
export const enum OpenMode {
|
||||
SINGLE_CLICK,
|
||||
DOUBLE_CLICK
|
||||
}
|
||||
@@ -191,9 +189,9 @@ export class DefaultController implements _.IController {
|
||||
|
||||
if (this.shouldToggleExpansion(element, event, origin)) {
|
||||
if (tree.isExpanded(element)) {
|
||||
tree.collapse(element).done(null, errors.onUnexpectedError);
|
||||
tree.collapse(element).then(null, errors.onUnexpectedError);
|
||||
} else {
|
||||
tree.expand(element).done(null, errors.onUnexpectedError);
|
||||
tree.expand(element).then(null, errors.onUnexpectedError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -264,8 +262,9 @@ export class DefaultController implements _.IController {
|
||||
}
|
||||
|
||||
private onKey(bindings: KeybindingDispatcher, tree: _.ITree, event: IKeyboardEvent): boolean {
|
||||
const handler = bindings.dispatch(event.toKeybinding());
|
||||
const handler: any = bindings.dispatch(event.toKeybinding());
|
||||
if (handler) {
|
||||
// TODO: TS 3.1 upgrade. Why are we checking against void?
|
||||
if (handler(tree, event)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -282,7 +281,7 @@ export class DefaultController implements _.IController {
|
||||
tree.clearHighlight(payload);
|
||||
} else {
|
||||
tree.focusPrevious(1, payload);
|
||||
tree.reveal(tree.getFocus()).done(null, errors.onUnexpectedError);
|
||||
tree.reveal(tree.getFocus()).then(null, errors.onUnexpectedError);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -294,7 +293,7 @@ export class DefaultController implements _.IController {
|
||||
tree.clearHighlight(payload);
|
||||
} else {
|
||||
tree.focusPreviousPage(payload);
|
||||
tree.reveal(tree.getFocus()).done(null, errors.onUnexpectedError);
|
||||
tree.reveal(tree.getFocus()).then(null, errors.onUnexpectedError);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -306,7 +305,7 @@ export class DefaultController implements _.IController {
|
||||
tree.clearHighlight(payload);
|
||||
} else {
|
||||
tree.focusNext(1, payload);
|
||||
tree.reveal(tree.getFocus()).done(null, errors.onUnexpectedError);
|
||||
tree.reveal(tree.getFocus()).then(null, errors.onUnexpectedError);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -318,7 +317,7 @@ export class DefaultController implements _.IController {
|
||||
tree.clearHighlight(payload);
|
||||
} else {
|
||||
tree.focusNextPage(payload);
|
||||
tree.reveal(tree.getFocus()).done(null, errors.onUnexpectedError);
|
||||
tree.reveal(tree.getFocus()).then(null, errors.onUnexpectedError);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -330,7 +329,7 @@ export class DefaultController implements _.IController {
|
||||
tree.clearHighlight(payload);
|
||||
} else {
|
||||
tree.focusFirst(payload);
|
||||
tree.reveal(tree.getFocus()).done(null, errors.onUnexpectedError);
|
||||
tree.reveal(tree.getFocus()).then(null, errors.onUnexpectedError);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -342,7 +341,7 @@ export class DefaultController implements _.IController {
|
||||
tree.clearHighlight(payload);
|
||||
} else {
|
||||
tree.focusLast(payload);
|
||||
tree.reveal(tree.getFocus()).done(null, errors.onUnexpectedError);
|
||||
tree.reveal(tree.getFocus()).then(null, errors.onUnexpectedError);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -360,7 +359,7 @@ export class DefaultController implements _.IController {
|
||||
return tree.reveal(tree.getFocus());
|
||||
}
|
||||
return undefined;
|
||||
}).done(null, errors.onUnexpectedError);
|
||||
}).then(null, errors.onUnexpectedError);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -378,7 +377,7 @@ export class DefaultController implements _.IController {
|
||||
return tree.reveal(tree.getFocus());
|
||||
}
|
||||
return undefined;
|
||||
}).done(null, errors.onUnexpectedError);
|
||||
}).then(null, errors.onUnexpectedError);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -559,9 +558,9 @@ export class CollapseAllAction extends Action {
|
||||
super('vs.tree.collapse', nls.localize('collapse', "Collapse"), 'monaco-tree-action collapse-all', enabled);
|
||||
}
|
||||
|
||||
public run(context?: any): TPromise<any> {
|
||||
public run(context?: any): Thenable<any> {
|
||||
if (this.viewer.getHighlight()) {
|
||||
return TPromise.as(null); // Global action disabled if user is in edit mode from another action
|
||||
return Promise.resolve(); // Global action disabled if user is in edit mode from another action
|
||||
}
|
||||
|
||||
this.viewer.collapseAll();
|
||||
@@ -570,6 +569,6 @@ export class CollapseAllAction extends Action {
|
||||
this.viewer.domFocus();
|
||||
this.viewer.focusFirst();
|
||||
|
||||
return TPromise.as(null);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as _ from 'vs/base/parts/tree/browser/tree';
|
||||
import * as Mouse from 'vs/base/browser/mouseEvent';
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./tree';
|
||||
import * as WinJS from 'vs/base/common/winjs.base';
|
||||
@@ -180,7 +179,7 @@ export class Tree implements _.ITree {
|
||||
return this.model.collapse(element, recursive);
|
||||
}
|
||||
|
||||
public collapseAll(elements: any[] = null, recursive: boolean = false): WinJS.Promise {
|
||||
public collapseAll(elements: any[] | null = null, recursive: boolean = false): WinJS.Promise {
|
||||
return this.model.collapseAll(elements, recursive);
|
||||
}
|
||||
|
||||
@@ -200,7 +199,7 @@ export class Tree implements _.ITree {
|
||||
return this.model.getExpandedElements();
|
||||
}
|
||||
|
||||
public reveal(element: any, relativeTop: number = null): WinJS.Promise {
|
||||
public reveal(element: any, relativeTop: number | null = null): WinJS.Promise {
|
||||
return this.model.reveal(element, relativeTop);
|
||||
}
|
||||
|
||||
@@ -211,7 +210,10 @@ export class Tree implements _.ITree {
|
||||
|
||||
public getFirstVisibleElement(): any {
|
||||
return this.view.getFirstVisibleElement();
|
||||
}
|
||||
|
||||
public getLastVisibleElement(): any {
|
||||
return this.view.getLastVisibleElement();
|
||||
}
|
||||
|
||||
public getScrollPosition(): number {
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as Assert from 'vs/base/common/assert';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { IDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
import * as arrays from 'vs/base/common/arrays';
|
||||
import { INavigator } from 'vs/base/common/iterator';
|
||||
import * as WinJS from 'vs/base/common/winjs.base';
|
||||
import * as _ from './tree';
|
||||
import { Event, Emitter, once, EventMultiplexer, Relay } from 'vs/base/common/event';
|
||||
// {{SQL CARBON EDIT}}
|
||||
@@ -82,22 +80,20 @@ export class Lock {
|
||||
return !!this.locks[item.id];
|
||||
}
|
||||
|
||||
public run(item: Item, fn: () => WinJS.Promise): WinJS.Promise {
|
||||
public run(item: Item, fn: () => Thenable<any>): Thenable<any> {
|
||||
var lock = this.getLock(item);
|
||||
|
||||
if (lock) {
|
||||
var unbindListener: IDisposable;
|
||||
|
||||
return new WinJS.TPromise((c, e) => {
|
||||
unbindListener = once(lock.onDispose)(() => {
|
||||
return new Promise((c, e) => {
|
||||
once(lock.onDispose)(() => {
|
||||
return this.run(item, fn).then(c, e);
|
||||
});
|
||||
}, () => { unbindListener.dispose(); });
|
||||
});
|
||||
}
|
||||
|
||||
var result: WinJS.Promise;
|
||||
var result: Thenable<any>;
|
||||
|
||||
return new WinJS.TPromise((c, e) => {
|
||||
return new Promise((c, e) => {
|
||||
|
||||
if (item.isDisposed()) {
|
||||
return e(new Error('Item is disposed.'));
|
||||
@@ -113,7 +109,7 @@ export class Lock {
|
||||
}).then(c, e);
|
||||
|
||||
return result;
|
||||
}, () => result.cancel());
|
||||
});
|
||||
}
|
||||
|
||||
private getLock(item: Item): LockData {
|
||||
@@ -351,25 +347,29 @@ export class Item {
|
||||
this.expanded = value;
|
||||
}
|
||||
|
||||
public reveal(relativeTop: number = null): void {
|
||||
public reveal(relativeTop: number | null = null): void {
|
||||
var eventData: IItemRevealEvent = { item: this, relativeTop: relativeTop };
|
||||
this._onDidReveal.fire(eventData);
|
||||
}
|
||||
|
||||
public expand(): WinJS.Promise {
|
||||
public expand(): Thenable<any> {
|
||||
if (this.isExpanded() || !this.doesHaveChildren || this.lock.isLocked(this)) {
|
||||
return WinJS.TPromise.as(false);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
var result = this.lock.run(this, () => {
|
||||
if (this.isExpanded() || !this.doesHaveChildren) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
var eventData: IItemExpandEvent = { item: this };
|
||||
var result: WinJS.Promise;
|
||||
var result: Thenable<any>;
|
||||
this._onExpand.fire(eventData);
|
||||
|
||||
if (this.needsChildrenRefresh) {
|
||||
result = this.refreshChildren(false, true, true);
|
||||
} else {
|
||||
result = WinJS.TPromise.as(null);
|
||||
result = Promise.resolve(null);
|
||||
}
|
||||
|
||||
return result.then(() => {
|
||||
@@ -400,9 +400,9 @@ export class Item {
|
||||
});
|
||||
}
|
||||
|
||||
public collapse(recursive: boolean = false): WinJS.Promise {
|
||||
public collapse(recursive: boolean = false): Thenable<any> {
|
||||
if (recursive) {
|
||||
var collapseChildrenPromise = WinJS.TPromise.as(null);
|
||||
var collapseChildrenPromise = Promise.resolve(null);
|
||||
this.forEachChild((child) => {
|
||||
collapseChildrenPromise = collapseChildrenPromise.then(() => child.collapse(true));
|
||||
});
|
||||
@@ -411,7 +411,7 @@ export class Item {
|
||||
});
|
||||
} else {
|
||||
if (!this.isExpanded() || this.lock.isLocked(this)) {
|
||||
return WinJS.TPromise.as(false);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return this.lock.run(this, () => {
|
||||
@@ -420,7 +420,7 @@ export class Item {
|
||||
this._setExpanded(false);
|
||||
this._onDidCollapse.fire(eventData);
|
||||
|
||||
return WinJS.TPromise.as(true);
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -456,10 +456,16 @@ export class Item {
|
||||
return this.height;
|
||||
}
|
||||
|
||||
private refreshChildren(recursive: boolean, safe: boolean = false, force: boolean = false): WinJS.Promise {
|
||||
private refreshChildren(recursive: boolean, safe: boolean = false, force: boolean = false): Thenable<any> {
|
||||
if (!force && !this.isExpanded()) {
|
||||
this.needsChildrenRefresh = true;
|
||||
return WinJS.TPromise.as(this);
|
||||
const setNeedsChildrenRefresh = (item: Item) => {
|
||||
item.needsChildrenRefresh = true;
|
||||
item.forEachChild(setNeedsChildrenRefresh);
|
||||
};
|
||||
|
||||
setNeedsChildrenRefresh(this);
|
||||
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
this.needsChildrenRefresh = false;
|
||||
@@ -468,20 +474,20 @@ export class Item {
|
||||
var eventData: IItemChildrenRefreshEvent = { item: this, isNested: safe };
|
||||
this._onRefreshChildren.fire(eventData);
|
||||
|
||||
var childrenPromise: WinJS.Promise;
|
||||
var childrenPromise: Thenable<any>;
|
||||
if (this.doesHaveChildren) {
|
||||
childrenPromise = this.context.dataSource.getChildren(this.context.tree, this.element);
|
||||
} else {
|
||||
childrenPromise = WinJS.TPromise.as([]);
|
||||
childrenPromise = Promise.resolve([]);
|
||||
}
|
||||
|
||||
const result = childrenPromise.then((elements: any[]) => {
|
||||
if (this.isDisposed() || this.registry.isDisposed()) {
|
||||
return WinJS.TPromise.as(null);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
if (!Array.isArray(elements)) {
|
||||
return WinJS.TPromise.wrapError(new Error('Please return an array of children.'));
|
||||
return Promise.reject(new Error('Please return an array of children.'));
|
||||
}
|
||||
|
||||
elements = !elements ? [] : elements.slice(0);
|
||||
@@ -512,12 +518,18 @@ export class Item {
|
||||
}
|
||||
|
||||
if (recursive) {
|
||||
return WinJS.Promise.join(this.mapEachChild((child) => {
|
||||
return Promise.all(this.mapEachChild((child) => {
|
||||
return child.doRefresh(recursive, true);
|
||||
}));
|
||||
} else {
|
||||
this.mapEachChild(child => child.updateVisibility());
|
||||
return WinJS.TPromise.as(null);
|
||||
return Promise.all(this.mapEachChild((child) => {
|
||||
if (child.isExpanded() && child.needsChildrenRefresh) {
|
||||
return child.doRefresh(recursive, true);
|
||||
} else {
|
||||
child.updateVisibility();
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -529,7 +541,7 @@ export class Item {
|
||||
return safe ? doRefresh() : this.lock.run(this, doRefresh);
|
||||
}
|
||||
|
||||
private doRefresh(recursive: boolean, safe: boolean = false): WinJS.Promise {
|
||||
private doRefresh(recursive: boolean, safe: boolean = false): Thenable<any> {
|
||||
this.doesHaveChildren = this.context.dataSource.hasChildren(this.context.tree, this.element);
|
||||
this.height = this._getHeight();
|
||||
this.updateVisibility();
|
||||
@@ -543,7 +555,7 @@ export class Item {
|
||||
this.setVisible(this._isVisible());
|
||||
}
|
||||
|
||||
public refresh(recursive: boolean): WinJS.Promise {
|
||||
public refresh(recursive: boolean): Thenable<any> {
|
||||
return this.doRefresh(recursive);
|
||||
}
|
||||
|
||||
@@ -834,8 +846,8 @@ function getRange(one: Item, other: Item): Item[] {
|
||||
var item = oneHierarchy[length - 1];
|
||||
var nav = item.getNavigator();
|
||||
|
||||
var oneIndex: number = null;
|
||||
var otherIndex: number = null;
|
||||
var oneIndex: number | null = null;
|
||||
var otherIndex: number | null = null;
|
||||
|
||||
var index = 0;
|
||||
var result: Item[] = [];
|
||||
@@ -926,7 +938,7 @@ export class TreeModel {
|
||||
this.traitsToItems = {};
|
||||
}
|
||||
|
||||
public setInput(element: any): WinJS.Promise {
|
||||
public setInput(element: any): Thenable<any> {
|
||||
var eventData: IInputEvent = { item: this.input };
|
||||
this._onSetInput.fire(eventData);
|
||||
|
||||
@@ -973,11 +985,11 @@ export class TreeModel {
|
||||
return this.input ? this.input.getElement() : null;
|
||||
}
|
||||
|
||||
public refresh(element: any = null, recursive: boolean = true): WinJS.Promise {
|
||||
public refresh(element: any = null, recursive: boolean = true): Thenable<any> {
|
||||
var item = this.getItem(element);
|
||||
|
||||
if (!item) {
|
||||
return WinJS.TPromise.as(null);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
var eventData: IRefreshEvent = { item: item, recursive: recursive };
|
||||
@@ -987,17 +999,17 @@ export class TreeModel {
|
||||
});
|
||||
}
|
||||
|
||||
public expand(element: any): WinJS.Promise {
|
||||
public expand(element: any): Thenable<any> {
|
||||
var item = this.getItem(element);
|
||||
|
||||
if (!item) {
|
||||
return WinJS.TPromise.as(false);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return item.expand();
|
||||
}
|
||||
|
||||
public expandAll(elements?: any[]): WinJS.Promise {
|
||||
public expandAll(elements?: any[]): Thenable<any> {
|
||||
if (!elements) {
|
||||
elements = [];
|
||||
|
||||
@@ -1009,24 +1021,54 @@ export class TreeModel {
|
||||
}
|
||||
}
|
||||
|
||||
return this._expandAll(elements);
|
||||
}
|
||||
|
||||
private _expandAll(elements: any[]): Thenable<any> {
|
||||
if (elements.length === 0) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const elementsToExpand: any[] = [];
|
||||
const elementsToDelay: any[] = [];
|
||||
|
||||
for (const element of elements) {
|
||||
var item = this.getItem(element);
|
||||
|
||||
if (item) {
|
||||
elementsToExpand.push(element);
|
||||
} else {
|
||||
elementsToDelay.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
if (elementsToExpand.length === 0) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return this.__expandAll(elementsToExpand)
|
||||
.then(() => this._expandAll(elementsToDelay));
|
||||
}
|
||||
|
||||
private __expandAll(elements: any[]): Thenable<any> {
|
||||
var promises = [];
|
||||
for (var i = 0, len = elements.length; i < len; i++) {
|
||||
promises.push(this.expand(elements[i]));
|
||||
}
|
||||
return WinJS.Promise.join(promises);
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
public collapse(element: any, recursive: boolean = false): WinJS.Promise {
|
||||
public collapse(element: any, recursive: boolean = false): Thenable<any> {
|
||||
var item = this.getItem(element);
|
||||
|
||||
if (!item) {
|
||||
return WinJS.TPromise.as(false);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return item.collapse(recursive);
|
||||
}
|
||||
|
||||
public collapseAll(elements: any[] = null, recursive: boolean = false): WinJS.Promise {
|
||||
public collapseAll(elements: any[] | null = null, recursive: boolean = false): Thenable<any> {
|
||||
if (!elements) {
|
||||
elements = [this.input];
|
||||
recursive = true;
|
||||
@@ -1035,19 +1077,19 @@ export class TreeModel {
|
||||
for (var i = 0, len = elements.length; i < len; i++) {
|
||||
promises.push(this.collapse(elements[i], recursive));
|
||||
}
|
||||
return WinJS.Promise.join(promises);
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
public toggleExpansion(element: any, recursive: boolean = false): WinJS.Promise {
|
||||
public toggleExpansion(element: any, recursive: boolean = false): Thenable<any> {
|
||||
return this.isExpanded(element) ? this.collapse(element, recursive) : this.expand(element);
|
||||
}
|
||||
|
||||
public toggleExpansionAll(elements: any[]): WinJS.Promise {
|
||||
public toggleExpansionAll(elements: any[]): Thenable<any> {
|
||||
var promises = [];
|
||||
for (var i = 0, len = elements.length; i < len; i++) {
|
||||
promises.push(this.toggleExpansion(elements[i]));
|
||||
}
|
||||
return WinJS.Promise.join(promises);
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
public isExpanded(element: any): boolean {
|
||||
@@ -1074,9 +1116,9 @@ export class TreeModel {
|
||||
return result;
|
||||
}
|
||||
|
||||
public reveal(element: any, relativeTop: number = null): WinJS.Promise {
|
||||
public reveal(element: any, relativeTop: number | null = null): Thenable<any> {
|
||||
return this.resolveUnknownParentChain(element).then((chain: any[]) => {
|
||||
var result = WinJS.TPromise.as(null);
|
||||
var result = Promise.resolve(null);
|
||||
|
||||
chain.forEach((e) => {
|
||||
result = result.then(() => this.expand(e));
|
||||
@@ -1092,10 +1134,10 @@ export class TreeModel {
|
||||
});
|
||||
}
|
||||
|
||||
private resolveUnknownParentChain(element: any): WinJS.Promise {
|
||||
private resolveUnknownParentChain(element: any): Thenable<any> {
|
||||
return this.context.dataSource.getParent(this.context.tree, element).then((parent) => {
|
||||
if (!parent) {
|
||||
return WinJS.TPromise.as([]);
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
return this.resolveUnknownParentChain(parent).then((result) => {
|
||||
@@ -1217,8 +1259,8 @@ export class TreeModel {
|
||||
|
||||
public selectPrevious(count: number = 1, clearSelection: boolean = true, eventPayload?: any): void {
|
||||
var selection = this.getSelection(),
|
||||
item: Item = null,
|
||||
previousItem: Item = null;
|
||||
item: Item | null = null,
|
||||
previousItem: Item | null = null;
|
||||
|
||||
if (selection.length === 0) {
|
||||
let nav = this.getNavigator(this.input);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as _ from 'vs/base/parts/tree/browser/tree';
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as Platform from 'vs/base/common/platform';
|
||||
import * as Browser from 'vs/base/browser/browser';
|
||||
import * as WinJS from 'vs/base/common/winjs.base';
|
||||
import * as Lifecycle from 'vs/base/common/lifecycle';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import * as Diff from 'vs/base/common/diff/diff';
|
||||
@@ -25,7 +23,7 @@ import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { DataTransfers } from 'vs/base/browser/dnd';
|
||||
import { DefaultTreestyler } from './treeDefaults';
|
||||
import { Delayer } from 'vs/base/common/async';
|
||||
import { Delayer, timeout } from 'vs/base/common/async';
|
||||
|
||||
export interface IRow {
|
||||
element: HTMLElement;
|
||||
@@ -59,10 +57,19 @@ export class RowCache implements Lifecycle.IDisposable {
|
||||
var row = document.createElement('div');
|
||||
row.appendChild(content);
|
||||
|
||||
let templateData: any = null;
|
||||
|
||||
try {
|
||||
templateData = this.context.renderer.renderTemplate(this.context.tree, templateId, content);
|
||||
} catch (err) {
|
||||
console.error('Tree usage error: exception while rendering template');
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
result = {
|
||||
element: row,
|
||||
templateId: templateId,
|
||||
templateData: this.context.renderer.renderTemplate(this.context.tree, templateId, content)
|
||||
templateData
|
||||
};
|
||||
}
|
||||
|
||||
@@ -120,7 +127,7 @@ export class ViewItem implements IViewItem {
|
||||
public needsRender: boolean;
|
||||
public uri: string;
|
||||
public unbindDragStart: Lifecycle.IDisposable;
|
||||
public loadingTimer: number;
|
||||
public loadingTimer: any;
|
||||
|
||||
public _styles: any;
|
||||
private _draggable: boolean;
|
||||
@@ -260,7 +267,12 @@ export class ViewItem implements IViewItem {
|
||||
this.element.style.width = 'fit-content';
|
||||
}
|
||||
|
||||
this.context.renderer.renderElement(this.context.tree, this.model.getElement(), this.templateId, this.row.templateData);
|
||||
try {
|
||||
this.context.renderer.renderElement(this.context.tree, this.model.getElement(), this.templateId, this.row.templateData);
|
||||
} catch (err) {
|
||||
console.error('Tree usage error: exception while rendering element');
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
if (this.context.horizontalScrolling) {
|
||||
this.width = DOM.getContentWidth(this.element) + paddingLeft;
|
||||
@@ -429,7 +441,7 @@ export class TreeView extends HeightMap {
|
||||
private currentDropTarget: ViewItem;
|
||||
private shouldInvalidateDropReaction: boolean;
|
||||
private currentDropTargets: ViewItem[];
|
||||
private currentDropPromise: WinJS.Promise;
|
||||
private currentDropDisposable: Lifecycle.IDisposable = Lifecycle.Disposable.None;
|
||||
private dragAndDropScrollInterval: number;
|
||||
private dragAndDropScrollTimeout: number;
|
||||
private dragAndDropMouseY: number;
|
||||
@@ -538,8 +550,12 @@ export class TreeView extends HeightMap {
|
||||
this.viewListeners.push(DOM.addDisposableListener(this.domNode, 'keyup', (e) => this.onKeyUp(e)));
|
||||
this.viewListeners.push(DOM.addDisposableListener(this.domNode, 'mousedown', (e) => this.onMouseDown(e)));
|
||||
this.viewListeners.push(DOM.addDisposableListener(this.domNode, 'mouseup', (e) => this.onMouseUp(e)));
|
||||
this.viewListeners.push(DOM.addDisposableListener(this.wrapper, 'auxclick', (e: MouseEvent) => {
|
||||
if (e && e.button === 1) {
|
||||
this.onMouseMiddleClick(e);
|
||||
}
|
||||
}));
|
||||
this.viewListeners.push(DOM.addDisposableListener(this.wrapper, 'click', (e) => this.onClick(e)));
|
||||
this.viewListeners.push(DOM.addDisposableListener(this.wrapper, 'auxclick', (e) => this.onClick(e))); // >= Chrome 56
|
||||
this.viewListeners.push(DOM.addDisposableListener(this.domNode, 'contextmenu', (e) => this.onContextMenu(e)));
|
||||
this.viewListeners.push(DOM.addDisposableListener(this.wrapper, Touch.EventType.Tap, (e) => this.onTap(e)));
|
||||
this.viewListeners.push(DOM.addDisposableListener(this.wrapper, Touch.EventType.Change, (e) => this.onTouchChange(e)));
|
||||
@@ -654,7 +670,23 @@ export class TreeView extends HeightMap {
|
||||
}
|
||||
|
||||
public getFirstVisibleElement(): any {
|
||||
const item = this.itemAtIndex(this.indexAt(this.lastRenderTop));
|
||||
const firstIndex = this.indexAt(this.lastRenderTop);
|
||||
let item = this.itemAtIndex(firstIndex);
|
||||
if (!item) {
|
||||
return item;
|
||||
}
|
||||
|
||||
const itemMidpoint = item.top + item.height / 2;
|
||||
if (itemMidpoint < this.scrollTop) {
|
||||
const nextItem = this.itemAtIndex(firstIndex + 1);
|
||||
item = nextItem || item;
|
||||
}
|
||||
|
||||
return item.model.getElement();
|
||||
}
|
||||
|
||||
public getLastVisibleElement(): any {
|
||||
const item = this.itemAtIndex(this.indexAt(this.lastRenderTop + this.lastRenderHeight - 1));
|
||||
return item && item.model.getElement();
|
||||
}
|
||||
|
||||
@@ -817,6 +849,7 @@ export class TreeView extends HeightMap {
|
||||
}
|
||||
|
||||
private set scrollHeight(scrollHeight: number) {
|
||||
scrollHeight = scrollHeight + (this.horizontalScrolling ? 10 : 0);
|
||||
this.scrollableElement.setScrollDimensions({ scrollHeight });
|
||||
}
|
||||
|
||||
@@ -839,12 +872,9 @@ export class TreeView extends HeightMap {
|
||||
}
|
||||
|
||||
public set scrollTop(scrollTop: number) {
|
||||
this.scrollableElement.setScrollDimensions({
|
||||
scrollHeight: this.getContentHeight()
|
||||
});
|
||||
this.scrollableElement.setScrollPosition({
|
||||
scrollTop: scrollTop
|
||||
});
|
||||
const scrollHeight = this.getContentHeight() + (this.horizontalScrolling ? 10 : 0);
|
||||
this.scrollableElement.setScrollDimensions({ scrollHeight });
|
||||
this.scrollableElement.setScrollPosition({ scrollTop });
|
||||
}
|
||||
|
||||
public getScrollPosition(): number {
|
||||
@@ -1184,6 +1214,20 @@ export class TreeView extends HeightMap {
|
||||
this.context.controller.onClick(this.context.tree, item.model.getElement(), event);
|
||||
}
|
||||
|
||||
private onMouseMiddleClick(e: MouseEvent): void {
|
||||
if (!this.context.controller.onMouseMiddleClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
var event = new Mouse.StandardMouseEvent(e);
|
||||
var item = this.getItemAround(event.target);
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
this.context.controller.onMouseMiddleClick(this.context.tree, item.model.getElement(), event);
|
||||
}
|
||||
|
||||
private onMouseDown(e: MouseEvent): void {
|
||||
this.didJustPressContextMenuKey = false;
|
||||
|
||||
@@ -1296,15 +1340,15 @@ export class TreeView extends HeightMap {
|
||||
|
||||
this.didJustPressContextMenuKey = event.keyCode === KeyCode.ContextMenu || (event.shiftKey && event.keyCode === KeyCode.F10);
|
||||
|
||||
if (event.target && event.target.tagName && event.target.tagName.toLowerCase() === 'input') {
|
||||
return; // Ignore event if target is a form input field (avoids browser specific issues)
|
||||
}
|
||||
|
||||
if (this.didJustPressContextMenuKey) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (event.target && event.target.tagName && event.target.tagName.toLowerCase() === 'input') {
|
||||
return; // Ignore event if target is a form input field (avoids browser specific issues)
|
||||
}
|
||||
|
||||
this.context.controller.onKeyDown(this.context.tree, event);
|
||||
}
|
||||
|
||||
@@ -1417,11 +1461,7 @@ export class TreeView extends HeightMap {
|
||||
|
||||
this.currentDropTargets.forEach(i => i.dropTarget = false);
|
||||
this.currentDropTargets = [];
|
||||
|
||||
if (this.currentDropPromise) {
|
||||
this.currentDropPromise.cancel();
|
||||
this.currentDropPromise = null;
|
||||
}
|
||||
this.currentDropDisposable.dispose();
|
||||
}
|
||||
|
||||
this.cancelDragAndDropScrollInterval();
|
||||
@@ -1494,11 +1534,7 @@ export class TreeView extends HeightMap {
|
||||
if (this.currentDropTarget) {
|
||||
this.currentDropTargets.forEach(i => i.dropTarget = false);
|
||||
this.currentDropTargets = [];
|
||||
|
||||
if (this.currentDropPromise) {
|
||||
this.currentDropPromise.cancel();
|
||||
this.currentDropPromise = null;
|
||||
}
|
||||
this.currentDropDisposable.dispose();
|
||||
}
|
||||
|
||||
this.currentDropTarget = currentDropTarget;
|
||||
@@ -1525,7 +1561,10 @@ export class TreeView extends HeightMap {
|
||||
}
|
||||
|
||||
if (reaction.autoExpand) {
|
||||
this.currentDropPromise = WinJS.TPromise.timeout(500)
|
||||
const timeoutPromise = timeout(500);
|
||||
this.currentDropDisposable = Lifecycle.toDisposable(() => timeoutPromise.cancel());
|
||||
|
||||
timeoutPromise
|
||||
.then(() => this.context.tree.expand(this.currentDropElement))
|
||||
.then(() => this.shouldInvalidateDropReaction = true);
|
||||
}
|
||||
@@ -1558,10 +1597,7 @@ export class TreeView extends HeightMap {
|
||||
this.context.dnd.dropAbort(this.context.tree, this.currentDragAndDropData);
|
||||
}
|
||||
|
||||
if (this.currentDropPromise) {
|
||||
this.currentDropPromise.cancel();
|
||||
this.currentDropPromise = null;
|
||||
}
|
||||
this.currentDropDisposable.dispose();
|
||||
|
||||
this.cancelDragAndDropScrollInterval();
|
||||
this.currentDragAndDropData = null;
|
||||
@@ -1625,7 +1661,7 @@ export class TreeView extends HeightMap {
|
||||
// DOM changes
|
||||
|
||||
private insertItemInDOM(item: ViewItem): void {
|
||||
var elementAfter: HTMLElement = null;
|
||||
var elementAfter: HTMLElement | null = null;
|
||||
var itemAfter = <ViewItem>this.itemAfter(item);
|
||||
|
||||
if (itemAfter && itemAfter.element) {
|
||||
@@ -1660,7 +1696,7 @@ export class TreeView extends HeightMap {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
if (element === document.body) {
|
||||
if (element === this.scrollableElement.getDomNode() || element === document.body) {
|
||||
return null;
|
||||
}
|
||||
} while (element = element.parentElement);
|
||||
|
||||
@@ -28,7 +28,7 @@ export class HeightMap {
|
||||
return !last ? 0 : last.top + last.height;
|
||||
}
|
||||
|
||||
public onInsertItems(iterator: INextIterator<Item>, afterItemId: string = null): number {
|
||||
public onInsertItems(iterator: INextIterator<Item>, afterItemId: string | null = null): number {
|
||||
var item: Item;
|
||||
var viewItem: IViewItem;
|
||||
var i: number, j: number;
|
||||
@@ -90,7 +90,7 @@ export class HeightMap {
|
||||
public onRemoveItems(iterator: INextIterator<string>): void {
|
||||
var itemId: string;
|
||||
var viewItem: IViewItem;
|
||||
var startIndex: number = null;
|
||||
var startIndex: number | null = null;
|
||||
var i: number;
|
||||
var sizeDiff = 0;
|
||||
|
||||
@@ -140,7 +140,7 @@ export class HeightMap {
|
||||
var item: Item;
|
||||
var viewItem: IViewItem;
|
||||
var newHeight: number;
|
||||
var i: number, j: number = null;
|
||||
var i: number, j: number | null = null;
|
||||
var cummDiff = 0;
|
||||
|
||||
while (item = iterator.next()) {
|
||||
|
||||
@@ -3,15 +3,13 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as lifecycle from 'vs/base/common/lifecycle';
|
||||
import * as _ from 'vs/base/parts/tree/browser/tree';
|
||||
import * as WinJS from 'vs/base/common/winjs.base';
|
||||
import * as model from 'vs/base/parts/tree/browser/treeModel';
|
||||
import * as TreeDefaults from 'vs/base/parts/tree/browser/treeDefaults';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
|
||||
export class FakeRenderer {
|
||||
|
||||
@@ -171,11 +169,11 @@ class TestDataSource implements _.IDataSource {
|
||||
return !!element.children;
|
||||
}
|
||||
|
||||
public getChildren(tree, element): WinJS.Promise {
|
||||
return WinJS.TPromise.as(element.children);
|
||||
public getChildren(tree, element): Thenable<any> {
|
||||
return Promise.resolve(element.children);
|
||||
}
|
||||
|
||||
public getParent(tree, element): WinJS.Promise {
|
||||
public getParent(tree, element): Thenable<any> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
@@ -255,14 +253,14 @@ suite('TreeModel', () => {
|
||||
|
||||
test('refresh(expanded element) refreshes the element and descendants', () => {
|
||||
return model.setInput(SAMPLE.AB).then(() => {
|
||||
model.expand(SAMPLE.AB.children[0]);
|
||||
|
||||
counter.listen(model.onRefresh); // 1
|
||||
counter.listen(model.onDidRefresh); // 1
|
||||
counter.listen(model.onDidRefreshItem); // 3
|
||||
counter.listen(model.onRefreshItemChildren); // 1
|
||||
counter.listen(model.onDidRefreshItemChildren); // 1
|
||||
return model.refresh(SAMPLE.AB.children[0]);
|
||||
return model.expand(SAMPLE.AB.children[0]).then(() => {
|
||||
counter.listen(model.onRefresh); // 1
|
||||
counter.listen(model.onDidRefresh); // 1
|
||||
counter.listen(model.onDidRefreshItem); // 3
|
||||
counter.listen(model.onRefreshItemChildren); // 1
|
||||
counter.listen(model.onDidRefreshItemChildren); // 1
|
||||
return model.refresh(SAMPLE.AB.children[0]);
|
||||
});
|
||||
}).then(() => {
|
||||
assert.equal(counter.count, 7);
|
||||
});
|
||||
@@ -270,17 +268,17 @@ suite('TreeModel', () => {
|
||||
|
||||
test('refresh(element, false) refreshes the element', () => {
|
||||
return model.setInput(SAMPLE.AB).then(() => {
|
||||
model.expand(SAMPLE.AB.children[0]);
|
||||
|
||||
counter.listen(model.onRefresh); // 1
|
||||
counter.listen(model.onDidRefresh); // 1
|
||||
counter.listen(model.onDidRefreshItem, item => { // 1
|
||||
assert.equal(item.id, 'a');
|
||||
counter.up();
|
||||
return model.expand(SAMPLE.AB.children[0]).then(() => {
|
||||
counter.listen(model.onRefresh); // 1
|
||||
counter.listen(model.onDidRefresh); // 1
|
||||
counter.listen(model.onDidRefreshItem, item => { // 1
|
||||
assert.equal(item.id, 'a');
|
||||
counter.up();
|
||||
});
|
||||
counter.listen(model.onRefreshItemChildren); // 1
|
||||
counter.listen(model.onDidRefreshItemChildren); // 1
|
||||
return model.refresh(SAMPLE.AB.children[0], false);
|
||||
});
|
||||
counter.listen(model.onRefreshItemChildren); // 1
|
||||
counter.listen(model.onDidRefreshItemChildren); // 1
|
||||
return model.refresh(SAMPLE.AB.children[0], false);
|
||||
}).then(() => {
|
||||
assert.equal(counter.count, 6);
|
||||
});
|
||||
@@ -681,11 +679,11 @@ suite('TreeModel - Expansion', () => {
|
||||
getId: (_, e) => e,
|
||||
hasChildren: (_, e) => true,
|
||||
getChildren: (_, e) => {
|
||||
if (e === 'root') { return WinJS.TPromise.wrap(['a', 'b', 'c']); }
|
||||
if (e === 'b') { return WinJS.TPromise.wrap(['b1']); }
|
||||
return WinJS.TPromise.as([]);
|
||||
if (e === 'root') { return Promise.resolve(['a', 'b', 'c']); }
|
||||
if (e === 'b') { return Promise.resolve(['b1']); }
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
getParent: (_, e): WinJS.Promise => { throw new Error('not implemented'); },
|
||||
getParent: (_, e): Thenable<any> => { throw new Error('not implemented'); },
|
||||
shouldAutoexpand: (_, e) => e === 'b'
|
||||
}
|
||||
});
|
||||
@@ -1081,7 +1079,7 @@ suite('TreeModel - Traits', () => {
|
||||
class DynamicModel implements _.IDataSource {
|
||||
|
||||
private data: any;
|
||||
public promiseFactory: { (): WinJS.Promise; };
|
||||
public promiseFactory: { (): Thenable<any>; };
|
||||
|
||||
private _onGetChildren = new Emitter<any>();
|
||||
readonly onGetChildren: Event<any> = this._onGetChildren.event;
|
||||
@@ -1126,16 +1124,16 @@ class DynamicModel implements _.IDataSource {
|
||||
return !!this.data[element];
|
||||
}
|
||||
|
||||
public getChildren(tree, element): WinJS.Promise {
|
||||
public getChildren(tree, element): Thenable<any> {
|
||||
this._onGetChildren.fire(element);
|
||||
var result = this.promiseFactory ? this.promiseFactory() : WinJS.TPromise.as(null);
|
||||
var result = this.promiseFactory ? this.promiseFactory() : Promise.resolve(null);
|
||||
return result.then(() => {
|
||||
this._onDidGetChildren.fire(element);
|
||||
return WinJS.TPromise.as(this.data[element]);
|
||||
return Promise.resolve(this.data[element]);
|
||||
});
|
||||
}
|
||||
|
||||
public getParent(tree, element): WinJS.Promise {
|
||||
public getParent(tree, element): Thenable<any> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
@@ -1274,27 +1272,28 @@ suite('TreeModel - Dynamic data model', () => {
|
||||
dataModel.addChild('father', 'son');
|
||||
|
||||
return model.setInput('root').then(() => {
|
||||
model.expand('grandfather');
|
||||
model.collapse('father');
|
||||
return model.expand('grandfather').then(() => {
|
||||
return model.collapse('father').then(() => {
|
||||
var times = 0;
|
||||
var listener = dataModel.onGetChildren((element) => {
|
||||
times++;
|
||||
assert.equal(element, 'grandfather');
|
||||
});
|
||||
|
||||
var times = 0;
|
||||
var listener = dataModel.onGetChildren((element) => {
|
||||
times++;
|
||||
assert.equal(element, 'grandfather');
|
||||
});
|
||||
return model.refresh('grandfather').then(() => {
|
||||
assert.equal(times, 1);
|
||||
listener.dispose();
|
||||
|
||||
return model.refresh('grandfather').then(() => {
|
||||
assert.equal(times, 1);
|
||||
listener.dispose();
|
||||
listener = dataModel.onGetChildren((element) => {
|
||||
times++;
|
||||
assert.equal(element, 'father');
|
||||
});
|
||||
|
||||
listener = dataModel.onGetChildren((element) => {
|
||||
times++;
|
||||
assert.equal(element, 'father');
|
||||
});
|
||||
|
||||
return model.expand('father').then(() => {
|
||||
assert.equal(times, 2);
|
||||
listener.dispose();
|
||||
return model.expand('father').then(() => {
|
||||
assert.equal(times, 2);
|
||||
listener.dispose();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1307,49 +1306,51 @@ suite('TreeModel - Dynamic data model', () => {
|
||||
dataModel.addChild('mother', 'daughter');
|
||||
|
||||
return model.setInput('root').then(() => {
|
||||
model.expand('father');
|
||||
model.expand('mother');
|
||||
return model.expand('father').then(() => {
|
||||
return model.expand('mother').then(() => {
|
||||
|
||||
var nav = model.getNavigator();
|
||||
assert.equal(nav.next().id, 'father');
|
||||
assert.equal(nav.next().id, 'son');
|
||||
assert.equal(nav.next().id, 'mother');
|
||||
assert.equal(nav.next().id, 'daughter');
|
||||
assert.equal(nav.next() && false, null);
|
||||
var nav = model.getNavigator();
|
||||
assert.equal(nav.next().id, 'father');
|
||||
assert.equal(nav.next().id, 'son');
|
||||
assert.equal(nav.next().id, 'mother');
|
||||
assert.equal(nav.next().id, 'daughter');
|
||||
assert.equal(nav.next() && false, null);
|
||||
|
||||
dataModel.removeChild('father', 'son');
|
||||
dataModel.removeChild('mother', 'daughter');
|
||||
dataModel.addChild('father', 'brother');
|
||||
dataModel.addChild('mother', 'sister');
|
||||
dataModel.removeChild('father', 'son');
|
||||
dataModel.removeChild('mother', 'daughter');
|
||||
dataModel.addChild('father', 'brother');
|
||||
dataModel.addChild('mother', 'sister');
|
||||
|
||||
dataModel.promiseFactory = () => { return WinJS.TPromise.timeout(0); };
|
||||
dataModel.promiseFactory = () => { return timeout(0); };
|
||||
|
||||
var getTimes = 0;
|
||||
var gotTimes = 0;
|
||||
var getListener = dataModel.onGetChildren((element) => { getTimes++; });
|
||||
var gotListener = dataModel.onDidGetChildren((element) => { gotTimes++; });
|
||||
var getTimes = 0;
|
||||
var gotTimes = 0;
|
||||
var getListener = dataModel.onGetChildren((element) => { getTimes++; });
|
||||
var gotListener = dataModel.onDidGetChildren((element) => { gotTimes++; });
|
||||
|
||||
var p1 = model.refresh('father');
|
||||
assert.equal(getTimes, 1);
|
||||
assert.equal(gotTimes, 0);
|
||||
var p1 = model.refresh('father');
|
||||
assert.equal(getTimes, 1);
|
||||
assert.equal(gotTimes, 0);
|
||||
|
||||
var p2 = model.refresh('mother');
|
||||
assert.equal(getTimes, 2);
|
||||
assert.equal(gotTimes, 0);
|
||||
var p2 = model.refresh('mother');
|
||||
assert.equal(getTimes, 2);
|
||||
assert.equal(gotTimes, 0);
|
||||
|
||||
return WinJS.Promise.join([p1, p2]).then(() => {
|
||||
assert.equal(getTimes, 2);
|
||||
assert.equal(gotTimes, 2);
|
||||
return Promise.all([p1, p2]).then(() => {
|
||||
assert.equal(getTimes, 2);
|
||||
assert.equal(gotTimes, 2);
|
||||
|
||||
nav = model.getNavigator();
|
||||
assert.equal(nav.next().id, 'father');
|
||||
assert.equal(nav.next().id, 'brother');
|
||||
assert.equal(nav.next().id, 'mother');
|
||||
assert.equal(nav.next().id, 'sister');
|
||||
assert.equal(nav.next() && false, null);
|
||||
nav = model.getNavigator();
|
||||
assert.equal(nav.next().id, 'father');
|
||||
assert.equal(nav.next().id, 'brother');
|
||||
assert.equal(nav.next().id, 'mother');
|
||||
assert.equal(nav.next().id, 'sister');
|
||||
assert.equal(nav.next() && false, null);
|
||||
|
||||
getListener.dispose();
|
||||
gotListener.dispose();
|
||||
getListener.dispose();
|
||||
gotListener.dispose();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1360,139 +1361,76 @@ suite('TreeModel - Dynamic data model', () => {
|
||||
dataModel.addChild('father', 'son');
|
||||
|
||||
return model.setInput('root').then(() => {
|
||||
model.expand('grandfather');
|
||||
model.expand('father');
|
||||
return model.expand('grandfather').then(() => {
|
||||
return model.expand('father').then(() => {
|
||||
var nav = model.getNavigator();
|
||||
assert.equal(nav.next().id, 'grandfather');
|
||||
assert.equal(nav.next().id, 'father');
|
||||
assert.equal(nav.next().id, 'son');
|
||||
assert.equal(nav.next() && false, null);
|
||||
|
||||
var nav = model.getNavigator();
|
||||
assert.equal(nav.next().id, 'grandfather');
|
||||
assert.equal(nav.next().id, 'father');
|
||||
assert.equal(nav.next().id, 'son');
|
||||
assert.equal(nav.next() && false, null);
|
||||
var refreshTimes = 0;
|
||||
counter.listen(model.onDidRefreshItem, (e) => { refreshTimes++; });
|
||||
|
||||
var refreshTimes = 0;
|
||||
counter.listen(model.onDidRefreshItem, (e) => { refreshTimes++; });
|
||||
var getTimes = 0;
|
||||
var getListener = dataModel.onGetChildren((element) => { getTimes++; });
|
||||
|
||||
var getTimes = 0;
|
||||
var getListener = dataModel.onGetChildren((element) => { getTimes++; });
|
||||
var gotTimes = 0;
|
||||
var gotListener = dataModel.onDidGetChildren((element) => { gotTimes++; });
|
||||
|
||||
var gotTimes = 0;
|
||||
var gotListener = dataModel.onDidGetChildren((element) => { gotTimes++; });
|
||||
var p1Completes = [];
|
||||
dataModel.promiseFactory = () => { return new Promise((c) => { p1Completes.push(c); }); };
|
||||
|
||||
var p1Completes = [];
|
||||
dataModel.promiseFactory = () => { return new WinJS.TPromise((c) => { p1Completes.push(c); }); };
|
||||
model.refresh('grandfather').then(() => {
|
||||
// just a single get
|
||||
assert.equal(refreshTimes, 1); // (+1) grandfather
|
||||
assert.equal(getTimes, 1);
|
||||
assert.equal(gotTimes, 0);
|
||||
|
||||
model.refresh('grandfather');
|
||||
// unblock the first get
|
||||
p1Completes.shift()();
|
||||
|
||||
// just a single get
|
||||
assert.equal(refreshTimes, 1); // (+1) grandfather
|
||||
assert.equal(getTimes, 1);
|
||||
assert.equal(gotTimes, 0);
|
||||
// once the first get is unblocked, the second get should appear
|
||||
assert.equal(refreshTimes, 2); // (+1) first father refresh
|
||||
assert.equal(getTimes, 2);
|
||||
assert.equal(gotTimes, 1);
|
||||
|
||||
// unblock the first get
|
||||
p1Completes.shift()();
|
||||
var p2Complete;
|
||||
dataModel.promiseFactory = () => { return new Promise((c) => { p2Complete = c; }); };
|
||||
var p2 = model.refresh('father');
|
||||
|
||||
// once the first get is unblocked, the second get should appear
|
||||
assert.equal(refreshTimes, 2); // (+1) first father refresh
|
||||
assert.equal(getTimes, 2);
|
||||
assert.equal(gotTimes, 1);
|
||||
// same situation still
|
||||
assert.equal(refreshTimes, 3); // (+1) second father refresh
|
||||
assert.equal(getTimes, 2);
|
||||
assert.equal(gotTimes, 1);
|
||||
|
||||
var p2Complete;
|
||||
dataModel.promiseFactory = () => { return new WinJS.TPromise((c) => { p2Complete = c; }); };
|
||||
var p2 = model.refresh('father');
|
||||
// unblock the second get
|
||||
p1Completes.shift()();
|
||||
|
||||
// same situation still
|
||||
assert.equal(refreshTimes, 3); // (+1) second father refresh
|
||||
assert.equal(getTimes, 2);
|
||||
assert.equal(gotTimes, 1);
|
||||
// the third get should have appeared, it should've been waiting for the second one
|
||||
assert.equal(refreshTimes, 4); // (+1) first son request
|
||||
assert.equal(getTimes, 3);
|
||||
assert.equal(gotTimes, 2);
|
||||
|
||||
// unblock the second get
|
||||
p1Completes.shift()();
|
||||
p2Complete();
|
||||
|
||||
// the third get should have appeared, it should've been waiting for the second one
|
||||
assert.equal(refreshTimes, 4); // (+1) first son request
|
||||
assert.equal(getTimes, 3);
|
||||
assert.equal(gotTimes, 2);
|
||||
// all good
|
||||
assert.equal(refreshTimes, 5); // (+1) second son request
|
||||
assert.equal(getTimes, 3);
|
||||
assert.equal(gotTimes, 3);
|
||||
|
||||
p2Complete();
|
||||
return p2.then(() => {
|
||||
nav = model.getNavigator();
|
||||
assert.equal(nav.next().id, 'grandfather');
|
||||
assert.equal(nav.next().id, 'father');
|
||||
assert.equal(nav.next().id, 'son');
|
||||
assert.equal(nav.next() && false, null);
|
||||
|
||||
// all good
|
||||
assert.equal(refreshTimes, 5); // (+1) second son request
|
||||
assert.equal(getTimes, 3);
|
||||
assert.equal(gotTimes, 3);
|
||||
|
||||
return p2.then(() => {
|
||||
nav = model.getNavigator();
|
||||
assert.equal(nav.next().id, 'grandfather');
|
||||
assert.equal(nav.next().id, 'father');
|
||||
assert.equal(nav.next().id, 'son');
|
||||
assert.equal(nav.next() && false, null);
|
||||
|
||||
getListener.dispose();
|
||||
gotListener.dispose();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('simultaneously recursively refreshing two intersecting elements should concatenate the refreshes - ancestor second', () => {
|
||||
dataModel.addChild('root', 'grandfather');
|
||||
dataModel.addChild('grandfather', 'father');
|
||||
dataModel.addChild('father', 'son');
|
||||
|
||||
return model.setInput('root').then(() => {
|
||||
model.expand('grandfather');
|
||||
model.expand('father');
|
||||
|
||||
var nav = model.getNavigator();
|
||||
assert.equal(nav.next().id, 'grandfather');
|
||||
assert.equal(nav.next().id, 'father');
|
||||
assert.equal(nav.next().id, 'son');
|
||||
assert.equal(nav.next() && false, null);
|
||||
|
||||
var getTimes = 0;
|
||||
var gotTimes = 0;
|
||||
var getListener = dataModel.onGetChildren((element) => { getTimes++; });
|
||||
var gotListener = dataModel.onDidGetChildren((element) => { gotTimes++; });
|
||||
var p2;
|
||||
|
||||
var p1Complete;
|
||||
dataModel.promiseFactory = () => { return new WinJS.TPromise((c) => { p1Complete = c; }); };
|
||||
|
||||
model.refresh('father');
|
||||
|
||||
assert.equal(getTimes, 1);
|
||||
assert.equal(gotTimes, 0);
|
||||
|
||||
var p2Completes = [];
|
||||
dataModel.promiseFactory = () => { return new WinJS.TPromise((c) => { p2Completes.push(c); }); };
|
||||
p2 = model.refresh('grandfather');
|
||||
|
||||
assert.equal(getTimes, 1);
|
||||
assert.equal(gotTimes, 0);
|
||||
|
||||
p1Complete();
|
||||
|
||||
assert.equal(getTimes, 2);
|
||||
assert.equal(gotTimes, 1);
|
||||
|
||||
p2Completes.shift()();
|
||||
|
||||
assert.equal(getTimes, 3);
|
||||
assert.equal(gotTimes, 2);
|
||||
|
||||
p2Completes.shift()();
|
||||
|
||||
assert.equal(getTimes, 3);
|
||||
assert.equal(gotTimes, 3);
|
||||
|
||||
return p2.then(() => {
|
||||
nav = model.getNavigator();
|
||||
assert.equal(nav.next().id, 'grandfather');
|
||||
assert.equal(nav.next().id, 'father');
|
||||
assert.equal(nav.next().id, 'son');
|
||||
assert.equal(nav.next() && false, null);
|
||||
|
||||
getListener.dispose();
|
||||
gotListener.dispose();
|
||||
getListener.dispose();
|
||||
gotListener.dispose();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1502,15 +1440,16 @@ suite('TreeModel - Dynamic data model', () => {
|
||||
dataModel.addChild('grandfather', 'father');
|
||||
|
||||
return model.setInput('root').then(() => {
|
||||
model.expand('grandfather');
|
||||
model.expand('father');
|
||||
return model.expand('grandfather').then(() => {
|
||||
return model.expand('father').then(() => {
|
||||
assert(!model.isExpanded('father'));
|
||||
|
||||
assert(!model.isExpanded('father'));
|
||||
dataModel.addChild('father', 'son');
|
||||
|
||||
dataModel.addChild('father', 'son');
|
||||
|
||||
return model.refresh('father').then(() => {
|
||||
assert(!model.isExpanded('father'));
|
||||
return model.refresh('father').then(() => {
|
||||
assert(!model.isExpanded('father'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1521,16 +1460,18 @@ suite('TreeModel - Dynamic data model', () => {
|
||||
dataModel.addChild('father', 'son');
|
||||
|
||||
return model.setInput('root').then(() => {
|
||||
model.expand('grandfather');
|
||||
model.expand('father');
|
||||
model.collapse('father');
|
||||
return model.expand('grandfather').then(() => {
|
||||
return model.expand('father').then(() => {
|
||||
return model.collapse('father').then(() => {
|
||||
assert(!model.isExpanded('father'));
|
||||
|
||||
assert(!model.isExpanded('father'));
|
||||
dataModel.addChild('father', 'daughter');
|
||||
|
||||
dataModel.addChild('father', 'daughter');
|
||||
|
||||
return model.refresh('father').then(() => {
|
||||
assert(!model.isExpanded('father'));
|
||||
return model.refresh('father').then(() => {
|
||||
assert(!model.isExpanded('father'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1541,15 +1482,16 @@ suite('TreeModel - Dynamic data model', () => {
|
||||
dataModel.addChild('father', 'son');
|
||||
|
||||
return model.setInput('root').then(() => {
|
||||
model.expand('grandfather');
|
||||
model.expand('father');
|
||||
return model.expand('grandfather').then(() => {
|
||||
return model.expand('father').then(() => {
|
||||
assert(model.isExpanded('grandfather'));
|
||||
assert(model.isExpanded('father'));
|
||||
|
||||
assert(model.isExpanded('grandfather'));
|
||||
assert(model.isExpanded('father'));
|
||||
|
||||
return model.refresh('grandfather').then(() => {
|
||||
assert(model.isExpanded('grandfather'));
|
||||
assert(model.isExpanded('father'));
|
||||
return model.refresh('grandfather').then(() => {
|
||||
assert(model.isExpanded('grandfather'));
|
||||
assert(model.isExpanded('father'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1560,16 +1502,18 @@ suite('TreeModel - Dynamic data model', () => {
|
||||
dataModel.addChild('father', 'son');
|
||||
|
||||
return model.setInput('root').then(() => {
|
||||
model.expand('grandfather');
|
||||
model.expand('father');
|
||||
model.collapse('father');
|
||||
return model.expand('grandfather').then(() => {
|
||||
return model.expand('father').then(() => {
|
||||
return model.collapse('father').then(() => {
|
||||
assert(model.isExpanded('grandfather'));
|
||||
assert(!model.isExpanded('father'));
|
||||
|
||||
assert(model.isExpanded('grandfather'));
|
||||
assert(!model.isExpanded('father'));
|
||||
|
||||
return model.refresh('grandfather').then(() => {
|
||||
assert(model.isExpanded('grandfather'));
|
||||
assert(!model.isExpanded('father'));
|
||||
return model.refresh('grandfather').then(() => {
|
||||
assert(model.isExpanded('grandfather'));
|
||||
assert(!model.isExpanded('father'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1583,9 +1527,9 @@ suite('TreeModel - Dynamic data model', () => {
|
||||
return model.setInput('root').then(() => {
|
||||
|
||||
// delay expansions and refreshes
|
||||
dataModel.promiseFactory = () => { return WinJS.TPromise.timeout(0); };
|
||||
dataModel.promiseFactory = () => { return timeout(0); };
|
||||
|
||||
var promises: WinJS.Promise[] = [];
|
||||
var promises: Thenable<any>[] = [];
|
||||
|
||||
promises.push(model.expand('father'));
|
||||
dataModel.removeChild('root', 'father');
|
||||
@@ -1595,7 +1539,7 @@ suite('TreeModel - Dynamic data model', () => {
|
||||
dataModel.removeChild('root', 'mother');
|
||||
promises.push(model.refresh('root'));
|
||||
|
||||
return WinJS.Promise.join(promises).then(() => {
|
||||
return Promise.all(promises).then(() => {
|
||||
assert(true, 'all good');
|
||||
}, (errs) => {
|
||||
assert(false, 'should not fail');
|
||||
@@ -1627,18 +1571,18 @@ suite('TreeModel - bugs', () => {
|
||||
getChildren: (_, e) => {
|
||||
if (e === 'root') { return getRootChildren(); }
|
||||
if (e === 'bart') { return getBartChildren(); }
|
||||
return WinJS.TPromise.as([]);
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
getParent: (_, e): WinJS.Promise => { throw new Error('not implemented'); },
|
||||
getParent: (_, e): Thenable<any> => { throw new Error('not implemented'); },
|
||||
}
|
||||
});
|
||||
|
||||
let listeners = <any>[];
|
||||
|
||||
// helpers
|
||||
var getGetRootChildren = (children: string[], timeout = 0) => () => WinJS.TPromise.timeout(timeout).then(() => children);
|
||||
var getGetRootChildren = (children: string[], millis = 0) => () => timeout(millis).then(() => children);
|
||||
var getRootChildren = getGetRootChildren(['homer', 'bart', 'lisa', 'marge', 'maggie'], 0);
|
||||
var getGetBartChildren = (timeout = 0) => () => WinJS.TPromise.timeout(timeout).then(() => ['milhouse', 'nelson']);
|
||||
var getGetBartChildren = (millis = 0) => () => timeout(millis).then(() => ['milhouse', 'nelson']);
|
||||
var getBartChildren = getGetBartChildren(0);
|
||||
|
||||
// item expanding should not exist!
|
||||
@@ -1665,7 +1609,7 @@ suite('TreeModel - bugs', () => {
|
||||
});
|
||||
|
||||
// what now?
|
||||
return WinJS.Promise.join([p1, p2]);
|
||||
return Promise.all([p1, p2]);
|
||||
|
||||
}).then(() => {
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { ArrayIterator } from 'vs/base/common/iterator';
|
||||
import { HeightMap, IViewItem } from 'vs/base/parts/tree/browser/treeViewModel';
|
||||
|
||||
Reference in New Issue
Block a user