SQL Operations Studio Public Preview 1 (0.23) release source code
38
src/vs/base/parts/ipc/common/ipc.electron.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
|
||||
export interface Sender {
|
||||
send(channel: string, ...args: any[]): void;
|
||||
}
|
||||
|
||||
export class Protocol implements IMessagePassingProtocol {
|
||||
|
||||
private listener: IDisposable;
|
||||
|
||||
private _onMessage: Event<any>;
|
||||
get onMessage(): Event<any> { return this._onMessage; }
|
||||
|
||||
constructor(private sender: Sender, private onMessageEvent: Event<any>) {
|
||||
const emitter = new Emitter<any>();
|
||||
onMessageEvent(msg => emitter.fire(msg));
|
||||
this._onMessage = emitter.event;
|
||||
}
|
||||
|
||||
send(message: any): void {
|
||||
try {
|
||||
this.sender.send('ipc:message', message);
|
||||
} catch (e) {
|
||||
// systems are going down
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.listener = dispose(this.listener);
|
||||
}
|
||||
}
|
||||
502
src/vs/base/parts/ipc/common/ipc.ts
Normal file
@@ -0,0 +1,502 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 } from 'vs/base/common/lifecycle';
|
||||
import Event, { Emitter, once, filterEvent } from 'vs/base/common/event';
|
||||
|
||||
enum MessageType {
|
||||
RequestCommon,
|
||||
RequestCancel,
|
||||
ResponseInitialize,
|
||||
ResponseSuccess,
|
||||
ResponseProgress,
|
||||
ResponseError,
|
||||
ResponseErrorObj
|
||||
}
|
||||
|
||||
function isResponse(messageType: MessageType): boolean {
|
||||
return messageType >= MessageType.ResponseInitialize;
|
||||
}
|
||||
|
||||
interface IRawMessage {
|
||||
id: number;
|
||||
type: MessageType;
|
||||
}
|
||||
|
||||
interface IRawRequest extends IRawMessage {
|
||||
channelName?: string;
|
||||
name?: string;
|
||||
arg?: any;
|
||||
}
|
||||
|
||||
interface IRequest {
|
||||
raw: IRawRequest;
|
||||
emitter?: Emitter<any>;
|
||||
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(command: string, arg?: any): TPromise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
route(command: string, arg: any): 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;
|
||||
}
|
||||
|
||||
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.RequestCommon:
|
||||
this.onCommonRequest(request);
|
||||
break;
|
||||
|
||||
case MessageType.RequestCancel:
|
||||
this.onCancelRequest(request);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private onCommonRequest(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.ResponseSuccess });
|
||||
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('\n') : void 0
|
||||
}, type: MessageType.ResponseError
|
||||
});
|
||||
} else {
|
||||
this.protocol.send(<IRawResponse>{ id, data, type: MessageType.ResponseErrorObj });
|
||||
}
|
||||
|
||||
delete this.activeRequests[request.id];
|
||||
}, data => {
|
||||
this.protocol.send(<IRawResponse>{ id, data, type: MessageType.ResponseProgress });
|
||||
});
|
||||
|
||||
this.activeRequests[request.id] = toDisposable(() => requestPromise.cancel());
|
||||
}
|
||||
|
||||
private onCancelRequest(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;
|
||||
private activeRequests: Promise[];
|
||||
private bufferedRequests: IRequest[];
|
||||
private handlers: { [id: number]: IHandler; };
|
||||
private lastRequestId: number;
|
||||
private protocolListener: IDisposable;
|
||||
|
||||
constructor(private protocol: IMessagePassingProtocol) {
|
||||
this.state = State.Uninitialized;
|
||||
this.activeRequests = [];
|
||||
this.bufferedRequests = [];
|
||||
this.handlers = Object.create(null);
|
||||
this.lastRequestId = 0;
|
||||
this.protocolListener = this.protocol.onMessage(r => this.onMessage(r));
|
||||
}
|
||||
|
||||
getChannel<T extends IChannel>(channelName: string): T {
|
||||
const call = (command, arg) => this.request(channelName, command, arg);
|
||||
return { call } as T;
|
||||
}
|
||||
|
||||
private request(channelName: string, name: string, arg: any): Promise {
|
||||
const request = {
|
||||
raw: {
|
||||
id: this.lastRequestId++,
|
||||
type: MessageType.RequestCommon,
|
||||
channelName,
|
||||
name,
|
||||
arg
|
||||
}
|
||||
};
|
||||
|
||||
const activeRequest = this.state === State.Uninitialized
|
||||
? this.bufferRequest(request)
|
||||
: this.doRequest(request);
|
||||
|
||||
this.activeRequests.push(activeRequest);
|
||||
|
||||
activeRequest
|
||||
.then(null, _ => null)
|
||||
.done(() => this.activeRequests = this.activeRequests.filter(i => i !== activeRequest));
|
||||
|
||||
return activeRequest;
|
||||
}
|
||||
|
||||
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.ResponseSuccess:
|
||||
delete this.handlers[id];
|
||||
c(response.data);
|
||||
break;
|
||||
|
||||
case MessageType.ResponseError:
|
||||
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.ResponseErrorObj:
|
||||
delete this.handlers[id];
|
||||
e(response.data);
|
||||
break;
|
||||
|
||||
case MessageType.ResponseProgress:
|
||||
p(response.data);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
this.send(request.raw);
|
||||
},
|
||||
() => this.send({ id, type: MessageType.RequestCancel }));
|
||||
}
|
||||
|
||||
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.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
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.protocolListener.dispose();
|
||||
this.protocolListener = null;
|
||||
|
||||
this.activeRequests.forEach(r => r.cancel());
|
||||
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 id = router.route(command, arg);
|
||||
|
||||
if (!id) {
|
||||
return TPromise.wrapError(new Error('Client id should be provided'));
|
||||
}
|
||||
|
||||
return this.getClient(id).then(client => client.getChannel(channelName).call(command, arg));
|
||||
};
|
||||
|
||||
return { call } as T;
|
||||
}
|
||||
|
||||
registerChannel(channelName: string, channel: IChannel): void {
|
||||
this.channels[channelName] = channel;
|
||||
}
|
||||
|
||||
private getClient(clientId: string): TPromise<IChannelClient> {
|
||||
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 = null;
|
||||
this.channelClients = 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, arg) => promise.then(c => c.call(command, arg));
|
||||
return { call } as T;
|
||||
}
|
||||
|
||||
export function getNextTickChannel<T extends IChannel>(channel: T): T {
|
||||
let didTick = false;
|
||||
|
||||
const call = (command, arg) => {
|
||||
if (didTick) {
|
||||
return channel.call(command, arg);
|
||||
}
|
||||
|
||||
return TPromise.timeout(0)
|
||||
.then(() => didTick = true)
|
||||
.then(() => channel.call(command, arg));
|
||||
};
|
||||
|
||||
return { call } as T;
|
||||
}
|
||||
|
||||
export type Serializer<T, R> = (obj: T) => R;
|
||||
export type Deserializer<T, R> = (raw: R) => T;
|
||||
|
||||
export function eventToCall<T>(event: Event<T>, serializer: Serializer<T, any> = t => t): TPromise<void> {
|
||||
let disposable: IDisposable;
|
||||
|
||||
return new TPromise(
|
||||
(c, e, p) => disposable = event(t => p(serializer(t))),
|
||||
() => disposable.dispose()
|
||||
);
|
||||
}
|
||||
|
||||
export function eventFromCall<T>(channel: IChannel, name: string, arg: any = null, deserializer: Deserializer<T, any> = t => t): Event<T> {
|
||||
let promise: Promise;
|
||||
|
||||
const emitter = new Emitter<any>({
|
||||
onFirstListenerAdd: () => {
|
||||
promise = channel.call(name, arg)
|
||||
.then(null, err => null, e => emitter.fire(deserializer(e)));
|
||||
},
|
||||
onLastListenerRemove: () => {
|
||||
promise.cancel();
|
||||
promise = null;
|
||||
}
|
||||
});
|
||||
|
||||
return emitter.event;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { fromEventEmitter } from 'vs/base/node/event';
|
||||
import { IPCClient } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Protocol } from 'vs/base/parts/ipc/common/ipc.electron';
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
export class Client extends IPCClient {
|
||||
|
||||
private static createProtocol(): Protocol {
|
||||
const onMessage = fromEventEmitter<string>(ipcRenderer, 'ipc:message', (_, message) => message);
|
||||
ipcRenderer.send('ipc:hello');
|
||||
return new Protocol(ipcRenderer, onMessage);
|
||||
}
|
||||
|
||||
constructor(id: string) {
|
||||
super(Client.createProtocol(), id);
|
||||
}
|
||||
}
|
||||
44
src/vs/base/parts/ipc/electron-main/ipc.electron-main.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Event, { filterEvent, mapEvent } from 'vs/base/common/event';
|
||||
import { fromEventEmitter } from 'vs/base/node/event';
|
||||
import { IPCServer, ClientConnectionEvent } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Protocol } from 'vs/base/parts/ipc/common/ipc.electron';
|
||||
import { ipcMain } from 'electron';
|
||||
|
||||
interface WebContents extends Electron.WebContents {
|
||||
getId(): number;
|
||||
}
|
||||
|
||||
interface IIPCEvent {
|
||||
event: { sender: WebContents; };
|
||||
message: string;
|
||||
}
|
||||
|
||||
function createScopedOnMessageEvent(senderId: number): Event<any> {
|
||||
const onMessage = fromEventEmitter<IIPCEvent>(ipcMain, 'ipc:message', (event, message) => ({ event, message }));
|
||||
const onMessageFromSender = filterEvent(onMessage, ({ event }) => event.sender.getId() === senderId);
|
||||
return mapEvent(onMessageFromSender, ({ message }) => message);
|
||||
}
|
||||
|
||||
export class Server extends IPCServer {
|
||||
|
||||
private static getOnDidClientConnect(): Event<ClientConnectionEvent> {
|
||||
const onHello = fromEventEmitter<WebContents>(ipcMain, 'ipc:hello', ({ sender }) => sender);
|
||||
|
||||
return mapEvent(onHello, webContents => {
|
||||
const onMessage = createScopedOnMessageEvent(webContents.getId());
|
||||
const protocol = new Protocol(webContents, onMessage);
|
||||
const onDidClientDisconnect = fromEventEmitter<void>(webContents, 'destroyed');
|
||||
|
||||
return { protocol, onDidClientDisconnect };
|
||||
});
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(Server.getOnDidClientConnect());
|
||||
}
|
||||
}
|
||||
220
src/vs/base/parts/ipc/node/ipc.cp.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChildProcess, fork } from 'child_process';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Delayer } from 'vs/base/common/async';
|
||||
import { clone, assign } from 'vs/base/common/objects';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { fromEventEmitter } from 'vs/base/node/event';
|
||||
import { createQueuedSender } from 'vs/base/node/processes';
|
||||
import { ChannelServer as IPCServer, ChannelClient as IPCClient, IChannelClient, IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
|
||||
export class Server extends IPCServer {
|
||||
constructor() {
|
||||
super({
|
||||
send: r => { try { process.send(r); } catch (e) { /* not much to do */ } },
|
||||
onMessage: fromEventEmitter(process, 'message', msg => msg)
|
||||
});
|
||||
|
||||
process.once('disconnect', () => this.dispose());
|
||||
}
|
||||
}
|
||||
|
||||
export interface IIPCOptions {
|
||||
|
||||
/**
|
||||
* A descriptive name for the server this connection is to. Used in logging.
|
||||
*/
|
||||
serverName: string;
|
||||
|
||||
/**
|
||||
* Time in millies before killing the ipc process. The next request after killing will start it again.
|
||||
*/
|
||||
timeout?: number;
|
||||
|
||||
/**
|
||||
* Arguments to the module to execute.
|
||||
*/
|
||||
args?: string[];
|
||||
|
||||
/**
|
||||
* Environment key-value pairs to be passed to the process that gets spawned for the ipc.
|
||||
*/
|
||||
env?: any;
|
||||
|
||||
/**
|
||||
* Allows to assign a debug port for debugging the application executed.
|
||||
*/
|
||||
debug?: number;
|
||||
|
||||
/**
|
||||
* Allows to assign a debug port for debugging the application and breaking it on the first line.
|
||||
*/
|
||||
debugBrk?: number;
|
||||
|
||||
/**
|
||||
* See https://github.com/Microsoft/vscode/issues/27665
|
||||
* Allows to pass in fresh execArgv to the forked process such that it doesn't inherit them from `process.execArgv`.
|
||||
* e.g. Launching the extension host process with `--inspect-brk=xxx` and then forking a process from the extension host
|
||||
* results in the forked process inheriting `--inspect-brk=xxx`.
|
||||
*/
|
||||
freshExecArgv?: boolean;
|
||||
|
||||
/**
|
||||
* Enables our createQueuedSender helper for this Client. Uses a queue when the internal Node.js queue is
|
||||
* full of messages - see notes on that method.
|
||||
*/
|
||||
useQueue?: boolean;
|
||||
}
|
||||
|
||||
export class Client implements IChannelClient, IDisposable {
|
||||
|
||||
private disposeDelayer: Delayer<void>;
|
||||
private activeRequests: TPromise<void>[];
|
||||
private child: ChildProcess;
|
||||
private _client: IPCClient;
|
||||
private channels: { [name: string]: IChannel };
|
||||
|
||||
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, arg) => this.request(channelName, command, arg);
|
||||
return { call } as T;
|
||||
}
|
||||
|
||||
protected request(channelName: string, name: string, arg: any): TPromise<void> {
|
||||
if (!this.disposeDelayer) {
|
||||
return TPromise.wrapError(new Error('disposed'));
|
||||
}
|
||||
|
||||
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(result), 1);
|
||||
|
||||
if (this.activeRequests.length === 0) {
|
||||
this.disposeDelayer.trigger(() => this.disposeClient());
|
||||
}
|
||||
});
|
||||
}, () => request.cancel());
|
||||
|
||||
this.activeRequests.push(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private get client(): IPCClient {
|
||||
if (!this._client) {
|
||||
const args = this.options && this.options.args ? this.options.args : [];
|
||||
const forkOpts = Object.create(null);
|
||||
|
||||
forkOpts.env = assign(clone(process.env), { 'VSCODE_PARENT_PID': String(process.pid) });
|
||||
|
||||
if (this.options && this.options.env) {
|
||||
forkOpts.env = assign(forkOpts.env, this.options.env);
|
||||
}
|
||||
|
||||
if (this.options && this.options.freshExecArgv) {
|
||||
forkOpts.execArgv = [];
|
||||
}
|
||||
|
||||
if (this.options && typeof this.options.debug === 'number') {
|
||||
forkOpts.execArgv = ['--nolazy', '--inspect=' + this.options.debug];
|
||||
}
|
||||
|
||||
if (this.options && typeof this.options.debugBrk === 'number') {
|
||||
forkOpts.execArgv = ['--nolazy', '--inspect-brk=' + this.options.debugBrk];
|
||||
}
|
||||
|
||||
this.child = fork(this.modulePath, args, forkOpts);
|
||||
|
||||
const onMessageEmitter = new Emitter<any>();
|
||||
const onRawMessage = fromEventEmitter(this.child, 'message', msg => msg);
|
||||
|
||||
onRawMessage(msg => {
|
||||
// Handle console logs specially
|
||||
if (msg && msg.type === '__$console') {
|
||||
let args = ['%c[IPC Library: ' + this.options.serverName + ']', 'color: darkgreen'];
|
||||
try {
|
||||
const parsed = JSON.parse(msg.arguments);
|
||||
args = args.concat(Object.getOwnPropertyNames(parsed).map(o => parsed[o]));
|
||||
} catch (error) {
|
||||
args.push(msg.arguments);
|
||||
}
|
||||
|
||||
console[msg.severity].apply(console, args);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Anything else goes to the outside
|
||||
else {
|
||||
onMessageEmitter.fire(msg);
|
||||
}
|
||||
});
|
||||
|
||||
const sender = this.options.useQueue ? createQueuedSender(this.child) : this.child;
|
||||
const send = r => this.child && this.child.connected && sender.send(r);
|
||||
const onMessage = onMessageEmitter.event;
|
||||
const protocol = { send, onMessage };
|
||||
|
||||
this._client = new IPCClient(protocol);
|
||||
|
||||
const onExit = () => this.disposeClient();
|
||||
process.once('exit', onExit);
|
||||
|
||||
this.child.on('error', err => console.warn('IPC "' + this.options.serverName + '" errored with ' + err));
|
||||
|
||||
this.child.on('exit', (code: any, signal: any) => {
|
||||
process.removeListener('exit', onExit);
|
||||
|
||||
if (this.activeRequests) {
|
||||
this.activeRequests.forEach(req => req.cancel());
|
||||
this.activeRequests = [];
|
||||
}
|
||||
|
||||
if (code !== 0 && signal !== 'SIGTERM') {
|
||||
console.warn('IPC "' + this.options.serverName + '" crashed with exit code ' + code);
|
||||
this.disposeDelayer.cancel();
|
||||
this.disposeClient();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return this._client;
|
||||
}
|
||||
|
||||
private disposeClient() {
|
||||
if (this._client) {
|
||||
this.child.kill();
|
||||
this.child = null;
|
||||
this._client = null;
|
||||
this.channels = Object.create(null);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposeDelayer.cancel();
|
||||
this.disposeDelayer = null;
|
||||
this.disposeClient();
|
||||
this.activeRequests = null;
|
||||
}
|
||||
}
|
||||
220
src/vs/base/parts/ipc/node/ipc.net.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Socket, Server as NetServer, createConnection, createServer } from 'net';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import Event, { Emitter, once, mapEvent } from 'vs/base/common/event';
|
||||
import { fromEventEmitter } from 'vs/base/node/event';
|
||||
import { IMessagePassingProtocol, ClientConnectionEvent, IPCServer, IPCClient } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
|
||||
export function generateRandomPipeName(): string {
|
||||
const randomSuffix = generateUuid();
|
||||
if (process.platform === 'win32') {
|
||||
return `\\\\.\\pipe\\vscode-${randomSuffix}-sock`;
|
||||
} else {
|
||||
// Mac/Unix: use socket file
|
||||
return join(tmpdir(), `vscode-${randomSuffix}.sock`);
|
||||
}
|
||||
}
|
||||
|
||||
export class Protocol implements IMessagePassingProtocol {
|
||||
|
||||
private static _headerLen = 17;
|
||||
|
||||
private _onMessage = new Emitter<any>();
|
||||
|
||||
readonly onMessage: Event<any> = this._onMessage.event;
|
||||
|
||||
constructor(private _socket: Socket) {
|
||||
|
||||
let chunks = [];
|
||||
let totalLength = 0;
|
||||
|
||||
const state = {
|
||||
readHead: true,
|
||||
bodyIsJson: false,
|
||||
bodyLen: -1,
|
||||
};
|
||||
|
||||
_socket.on('data', (data: Buffer) => {
|
||||
|
||||
chunks.push(data);
|
||||
totalLength += data.length;
|
||||
|
||||
while (totalLength > 0) {
|
||||
|
||||
if (state.readHead) {
|
||||
// expecting header -> read 17bytes for header
|
||||
// information: `bodyIsJson` and `bodyLen`
|
||||
if (totalLength >= Protocol._headerLen) {
|
||||
const all = Buffer.concat(chunks);
|
||||
|
||||
state.bodyIsJson = all.readInt8(0) === 1;
|
||||
state.bodyLen = all.readInt32BE(1);
|
||||
state.readHead = false;
|
||||
|
||||
const rest = all.slice(Protocol._headerLen);
|
||||
totalLength = rest.length;
|
||||
chunks = [rest];
|
||||
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.readHead) {
|
||||
// expecting body -> read bodyLen-bytes for
|
||||
// the actual message or wait for more data
|
||||
if (totalLength >= state.bodyLen) {
|
||||
|
||||
const all = Buffer.concat(chunks);
|
||||
let message = all.toString('utf8', 0, state.bodyLen);
|
||||
if (state.bodyIsJson) {
|
||||
message = JSON.parse(message);
|
||||
}
|
||||
this._onMessage.fire(message);
|
||||
|
||||
const rest = all.slice(state.bodyLen);
|
||||
totalLength = rest.length;
|
||||
chunks = [rest];
|
||||
|
||||
state.bodyIsJson = false;
|
||||
state.bodyLen = -1;
|
||||
state.readHead = true;
|
||||
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
const data = Buffer.from(message);
|
||||
header.writeInt32BE(data.length, 1);
|
||||
|
||||
this._writeSoon(header, data);
|
||||
}
|
||||
|
||||
private _writeBuffer = new class {
|
||||
|
||||
private _data: Buffer[] = [];
|
||||
private _totalLength = 0;
|
||||
|
||||
add(head: Buffer, body: Buffer): boolean {
|
||||
const wasEmpty = this._totalLength === 0;
|
||||
this._data.push(head, body);
|
||||
this._totalLength += head.length + body.length;
|
||||
return wasEmpty;
|
||||
}
|
||||
|
||||
take(): Buffer {
|
||||
const ret = Buffer.concat(this._data, this._totalLength);
|
||||
this._data.length = 0;
|
||||
this._totalLength = 0;
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
private _writeSoon(header: Buffer, data: Buffer): void {
|
||||
if (this._writeBuffer.add(header, data)) {
|
||||
setImmediate(() => {
|
||||
// return early if socket has been destroyed in the meantime
|
||||
if (this._socket.destroyed) {
|
||||
return;
|
||||
}
|
||||
// we ignore the returned value from `write` because we would have to cached the data
|
||||
// anyways and nodejs is already doing that for us:
|
||||
// > https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback
|
||||
// > However, the false return value is only advisory and the writable stream will unconditionally
|
||||
// > accept and buffer chunk even if it has not not been allowed to drain.
|
||||
this._socket.write(this._writeBuffer.take());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Server extends IPCServer {
|
||||
|
||||
private static toClientConnectionEvent(server: NetServer): Event<ClientConnectionEvent> {
|
||||
const onConnection = fromEventEmitter<Socket>(server, 'connection');
|
||||
|
||||
return mapEvent(onConnection, socket => ({
|
||||
protocol: new Protocol(socket),
|
||||
onDidClientDisconnect: once(fromEventEmitter<void>(socket, 'close'))
|
||||
}));
|
||||
}
|
||||
|
||||
constructor(private server: NetServer) {
|
||||
super(Server.toClientConnectionEvent(server));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
this.server.close();
|
||||
this.server = null;
|
||||
}
|
||||
}
|
||||
|
||||
export class Client extends IPCClient {
|
||||
|
||||
private _onClose = new Emitter<void>();
|
||||
get onClose(): Event<void> { return this._onClose.event; }
|
||||
|
||||
constructor(private socket: Socket, id: string) {
|
||||
super(new Protocol(socket), id);
|
||||
socket.once('close', () => this._onClose.fire());
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
this.socket.end();
|
||||
this.socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
const server = createServer();
|
||||
|
||||
server.on('error', e);
|
||||
server.listen(hook, () => {
|
||||
server.removeListener('error', e);
|
||||
c(new Server(server));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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) => {
|
||||
const socket = createConnection(hook, () => {
|
||||
socket.removeListener('error', e);
|
||||
c(new Client(socket, clientId));
|
||||
});
|
||||
|
||||
socket.once('error', e);
|
||||
});
|
||||
}
|
||||
90
src/vs/base/parts/ipc/test/node/ipc.net.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { 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';
|
||||
|
||||
class MockDuplex extends EventEmitter {
|
||||
|
||||
private _cache: Buffer[] = [];
|
||||
|
||||
readonly destroyed = false;
|
||||
|
||||
private _deliver(): void {
|
||||
if (this._cache.length) {
|
||||
const data = Buffer.concat(this._cache);
|
||||
this._cache.length = 0;
|
||||
this.emit('data', data);
|
||||
}
|
||||
}
|
||||
|
||||
write(data: Buffer, cb?: Function): boolean {
|
||||
this._cache.push(data);
|
||||
setImmediate(() => this._deliver());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suite('IPC, Socket Protocol', () => {
|
||||
|
||||
let stream: Socket;
|
||||
|
||||
setup(() => {
|
||||
stream = <any>new MockDuplex();
|
||||
});
|
||||
|
||||
test('read/write', () => {
|
||||
|
||||
const a = new Protocol(stream);
|
||||
const b = new Protocol(stream);
|
||||
|
||||
return new TPromise(resolve => {
|
||||
const sub = b.onMessage(data => {
|
||||
sub.dispose();
|
||||
assert.equal(data, '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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test('read/write, object data', () => {
|
||||
|
||||
const a = new Protocol(stream);
|
||||
const b = new Protocol(stream);
|
||||
|
||||
const data = {
|
||||
pi: Math.PI,
|
||||
foo: 'bar',
|
||||
more: true,
|
||||
data: 'Hello World'.split('')
|
||||
};
|
||||
|
||||
a.send(data);
|
||||
|
||||
return new TPromise(resolve => {
|
||||
b.onMessage(msg => {
|
||||
assert.deepEqual(msg, data);
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
114
src/vs/base/parts/ipc/test/node/ipc.perf.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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;
|
||||
});
|
||||
}
|
||||
});
|
||||
120
src/vs/base/parts/ipc/test/node/ipc.test.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { 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';
|
||||
|
||||
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 }
|
||||
});
|
||||
}
|
||||
|
||||
suite('IPC', () => {
|
||||
suite('child process', () => {
|
||||
|
||||
test('createChannel', () => {
|
||||
if (process.env['VSCODE_PID']) {
|
||||
return undefined; // this test fails when run from within VS Code
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
const channel = client.getChannel<ITestChannel>('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('cancellation', () => {
|
||||
if (process.env['VSCODE_PID']) {
|
||||
return undefined; // this test fails when run from within VS Code
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
const channel = client.getChannel<ITestChannel>('test');
|
||||
const service = new TestServiceClient(channel);
|
||||
const res = service.cancelMe();
|
||||
|
||||
setTimeout(() => res.cancel(), 50);
|
||||
|
||||
const result = res.then(
|
||||
() => assert.fail('Unexpected'),
|
||||
err => assert.ok(err && isPromiseCanceledError(err))
|
||||
);
|
||||
|
||||
return always(result, () => client.dispose());
|
||||
});
|
||||
|
||||
test('events', () => {
|
||||
if (process.env['VSCODE_PID']) {
|
||||
return undefined; // this test fails when run from within VS Code
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
const channel = client.getChannel<ITestChannel>('test');
|
||||
const service = new TestServiceClient(channel);
|
||||
|
||||
const event = new TPromise((c, e) => {
|
||||
service.onMarco(({ answer }) => {
|
||||
try {
|
||||
assert.equal(answer, 'polo');
|
||||
c(null);
|
||||
} catch (err) {
|
||||
e(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const request = service.marco();
|
||||
const result = TPromise.join<any>([request, event]);
|
||||
|
||||
return always(result, () => client.dispose());
|
||||
});
|
||||
|
||||
test('event dispose', () => {
|
||||
if (process.env['VSCODE_PID']) {
|
||||
return undefined; // this test fails when run from within VS Code
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
const channel = client.getChannel<ITestChannel>('test');
|
||||
const service = new TestServiceClient(channel);
|
||||
|
||||
let count = 0;
|
||||
const disposable = service.onMarco(() => count++);
|
||||
|
||||
const result = service.marco().then(answer => {
|
||||
assert.equal(answer, 'polo');
|
||||
assert.equal(count, 1);
|
||||
|
||||
return service.marco().then(answer => {
|
||||
assert.equal(answer, 'polo');
|
||||
assert.equal(count, 2);
|
||||
disposable.dispose();
|
||||
|
||||
return service.marco().then(answer => {
|
||||
assert.equal(answer, 'polo');
|
||||
assert.equal(count, 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return always(result, () => client.dispose());
|
||||
});
|
||||
});
|
||||
});
|
||||
12
src/vs/base/parts/ipc/test/node/testApp.ts
Normal file
@@ -0,0 +1,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 { Server } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { TestChannel, TestService } from './testService';
|
||||
|
||||
const server = new Server();
|
||||
const service = new TestService();
|
||||
server.registerChannel('test', new TestChannel(service));
|
||||
118
src/vs/base/parts/ipc/test/node/testService.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
|
||||
export interface IMarcoPoloEvent {
|
||||
answer: string;
|
||||
}
|
||||
|
||||
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[]>;
|
||||
}
|
||||
|
||||
export class TestService implements ITestService {
|
||||
|
||||
private _onMarco = new Emitter<IMarcoPoloEvent>();
|
||||
onMarco: Event<IMarcoPoloEvent> = this._onMarco.event;
|
||||
|
||||
private _data = 'abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
marco(): TPromise<string> {
|
||||
this._onMarco.fire({ answer: 'polo' });
|
||||
return TPromise.as('polo');
|
||||
}
|
||||
|
||||
pong(ping: string): TPromise<{ incoming: string, outgoing: string }> {
|
||||
return TPromise.as({ 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface ITestChannel extends IChannel {
|
||||
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 {
|
||||
|
||||
constructor(private testService: ITestService) { }
|
||||
|
||||
call(command: string, ...args: any[]): TPromise<any> {
|
||||
switch (command) {
|
||||
case 'pong': return this.testService.pong(args[0]);
|
||||
case 'cancelMe': return this.testService.cancelMe();
|
||||
case 'marco': return this.testService.marco();
|
||||
case 'event:marco': return eventToCall(this.testService.onMarco);
|
||||
case 'batchPerf': return this.testService.batchPerf(args[0].batches, args[0].size, args[0].dataSize);
|
||||
default: return TPromise.wrapError(new Error('command not found'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class TestServiceClient implements ITestService {
|
||||
|
||||
private _onMarco: Event<IMarcoPoloEvent>;
|
||||
get onMarco(): Event<IMarcoPoloEvent> { return this._onMarco; };
|
||||
|
||||
constructor(private channel: ITestChannel) {
|
||||
this._onMarco = eventFromCall<IMarcoPoloEvent>(channel, 'event:marco');
|
||||
}
|
||||
|
||||
marco(): TPromise<string> {
|
||||
return this.channel.call('marco');
|
||||
}
|
||||
|
||||
pong(ping: string): TPromise<{ incoming: string, outgoing: string }> {
|
||||
return this.channel.call('pong', ping);
|
||||
}
|
||||
|
||||
cancelMe(): TPromise<boolean> {
|
||||
return this.channel.call('cancelMe');
|
||||
}
|
||||
|
||||
batchPerf(batches: number, size: number, dataSize: number): PPromise<any, any[]> {
|
||||
return this.channel.call('batchPerf', { batches, size, dataSize });
|
||||
}
|
||||
}
|
||||
688
src/vs/base/parts/quickopen/browser/quickOpenModel.ts
Normal file
@@ -0,0 +1,688 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 nls = require('vs/nls');
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import types = require('vs/base/common/types');
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { ITree, IActionProvider } from 'vs/base/parts/tree/browser/tree';
|
||||
import filters = require('vs/base/common/filters');
|
||||
import strings = require('vs/base/common/strings');
|
||||
import paths = require('vs/base/common/paths');
|
||||
import { IconLabel, IIconLabelOptions } 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, compareByScore as doCompareByScore } from 'vs/base/common/comparers';
|
||||
import { ActionBar, IActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
|
||||
import DOM = require('vs/base/browser/dom');
|
||||
import { IQuickOpenStyles } from 'vs/base/parts/quickopen/browser/quickOpenWidget';
|
||||
import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel';
|
||||
import { OS } from 'vs/base/common/platform';
|
||||
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
|
||||
export interface IContext {
|
||||
event: any;
|
||||
quickNavigateConfiguration: IQuickNavigateConfiguration;
|
||||
}
|
||||
|
||||
export interface IHighlight {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
let IDS = 0;
|
||||
|
||||
class EntryAccessor {
|
||||
|
||||
public static getLabel(entry: QuickOpenEntry) {
|
||||
return entry.getLabel();
|
||||
}
|
||||
|
||||
public static getResourcePath(entry: QuickOpenEntry) {
|
||||
const resource = entry.getResource();
|
||||
return resource && resource.fsPath;
|
||||
}
|
||||
}
|
||||
|
||||
export class QuickOpenEntry {
|
||||
private id: string;
|
||||
private labelHighlights: IHighlight[];
|
||||
private descriptionHighlights: IHighlight[];
|
||||
private detailHighlights: IHighlight[];
|
||||
private hidden: boolean;
|
||||
|
||||
constructor(highlights: IHighlight[] = []) {
|
||||
this.id = (IDS++).toString();
|
||||
this.labelHighlights = highlights;
|
||||
this.descriptionHighlights = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* A unique identifier for the entry
|
||||
*/
|
||||
public getId(): string {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* The label of the entry to identify it from others in the list
|
||||
*/
|
||||
public getLabel(): string {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The options for the label to use for this entry
|
||||
*/
|
||||
public getLabelOptions(): IIconLabelOptions {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The label of the entry to use when a screen reader wants to read about the entry
|
||||
*/
|
||||
public getAriaLabel(): string {
|
||||
return this.getLabel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detail information about the entry that is optional and can be shown below the label
|
||||
*/
|
||||
public getDetail(): string {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The icon of the entry to identify it from others in the list
|
||||
*/
|
||||
public getIcon(): string {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A secondary description that is optional and can be shown right to the label
|
||||
*/
|
||||
public getDescription(): string {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* An optional keybinding to show for an entry.
|
||||
*/
|
||||
public getKeybinding(): ResolvedKeybinding {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A resource for this entry. Resource URIs can be used to compare different kinds of entries and group
|
||||
* them together.
|
||||
*/
|
||||
public getResource(): URI {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to reuse the same model while filtering. Hidden entries will not show up in the viewer.
|
||||
*/
|
||||
public isHidden(): boolean {
|
||||
return this.hidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to reuse the same model while filtering. Hidden entries will not show up in the viewer.
|
||||
*/
|
||||
public setHidden(hidden: boolean): void {
|
||||
this.hidden = hidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to set highlight ranges that should show up for the entry label and optionally description if set.
|
||||
*/
|
||||
public setHighlights(labelHighlights: IHighlight[], descriptionHighlights?: IHighlight[], detailHighlights?: IHighlight[]): void {
|
||||
this.labelHighlights = labelHighlights;
|
||||
this.descriptionHighlights = descriptionHighlights;
|
||||
this.detailHighlights = detailHighlights;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to return highlight ranges that should show up for the entry label and description.
|
||||
*/
|
||||
public getHighlights(): [IHighlight[] /* Label */, IHighlight[] /* Description */, IHighlight[] /* Detail */] {
|
||||
return [this.labelHighlights, this.descriptionHighlights, this.detailHighlights];
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the entry is selected for opening. Returns a boolean value indicating if an action was performed or not.
|
||||
* The mode parameter gives an indication if the element is previewed (using arrow keys) or opened.
|
||||
*
|
||||
* The context parameter provides additional context information how the run was triggered.
|
||||
*/
|
||||
public run(mode: Mode, context: IContext): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* A good default sort implementation for quick open entries respecting highlight information
|
||||
* as well as associated resources.
|
||||
*/
|
||||
public static compare(elementA: QuickOpenEntry, elementB: QuickOpenEntry, lookFor: string): number {
|
||||
|
||||
// Give matches with label highlights higher priority over
|
||||
// those with only description highlights
|
||||
const labelHighlightsA = elementA.getHighlights()[0] || [];
|
||||
const labelHighlightsB = elementB.getHighlights()[0] || [];
|
||||
if (labelHighlightsA.length && !labelHighlightsB.length) {
|
||||
return -1;
|
||||
} else if (!labelHighlightsA.length && labelHighlightsB.length) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Fallback to the full path if labels are identical and we have associated resources
|
||||
let nameA = elementA.getLabel();
|
||||
let nameB = elementB.getLabel();
|
||||
if (nameA === nameB) {
|
||||
const resourceA = elementA.getResource();
|
||||
const resourceB = elementB.getResource();
|
||||
|
||||
if (resourceA && resourceB) {
|
||||
nameA = resourceA.fsPath;
|
||||
nameB = resourceB.fsPath;
|
||||
}
|
||||
}
|
||||
|
||||
return compareAnything(nameA, nameB, lookFor);
|
||||
}
|
||||
|
||||
public static compareByScore(elementA: QuickOpenEntry, elementB: QuickOpenEntry, lookFor: string, lookForNormalizedLower: string, scorerCache?: { [key: string]: number }): number {
|
||||
return doCompareByScore(elementA, elementB, EntryAccessor, lookFor, lookForNormalizedLower, scorerCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* A good default highlight implementation for an entry with label and description.
|
||||
*/
|
||||
public static highlight(entry: QuickOpenEntry, lookFor: string, fuzzyHighlight = false): { labelHighlights: IHighlight[], descriptionHighlights: IHighlight[] } {
|
||||
let labelHighlights: IHighlight[] = [];
|
||||
const descriptionHighlights: IHighlight[] = [];
|
||||
|
||||
const normalizedLookFor = strings.stripWildcards(lookFor);
|
||||
const label = entry.getLabel();
|
||||
const description = entry.getDescription();
|
||||
|
||||
// Highlight file aware
|
||||
if (entry.getResource()) {
|
||||
|
||||
// Highlight entire label and description if searching for full absolute path
|
||||
const fsPath = entry.getResource().fsPath;
|
||||
if (lookFor.length === fsPath.length && lookFor.toLowerCase() === fsPath.toLowerCase()) {
|
||||
labelHighlights.push({ start: 0, end: label.length });
|
||||
descriptionHighlights.push({ start: 0, end: description.length });
|
||||
}
|
||||
|
||||
// Fuzzy/Full-Path: Highlight is special
|
||||
else if (fuzzyHighlight || lookFor.indexOf(paths.nativeSep) >= 0) {
|
||||
const candidateLabelHighlights = filters.matchesFuzzy(lookFor, label, fuzzyHighlight);
|
||||
if (!candidateLabelHighlights) {
|
||||
const pathPrefix = description ? (description + paths.nativeSep) : '';
|
||||
const pathPrefixLength = pathPrefix.length;
|
||||
|
||||
// If there are no highlights in the label, build a path out of description and highlight and match on both,
|
||||
// then extract the individual label and description highlights back to the original positions
|
||||
let pathHighlights = filters.matchesFuzzy(lookFor, pathPrefix + label, fuzzyHighlight);
|
||||
if (!pathHighlights && lookFor !== normalizedLookFor) {
|
||||
pathHighlights = filters.matchesFuzzy(normalizedLookFor, pathPrefix + label, fuzzyHighlight);
|
||||
}
|
||||
|
||||
if (pathHighlights) {
|
||||
pathHighlights.forEach(h => {
|
||||
|
||||
// Match overlaps label and description part, we need to split it up
|
||||
if (h.start < pathPrefixLength && h.end > pathPrefixLength) {
|
||||
labelHighlights.push({ start: 0, end: h.end - pathPrefixLength });
|
||||
descriptionHighlights.push({ start: h.start, end: pathPrefixLength });
|
||||
}
|
||||
|
||||
// Match on label part
|
||||
else if (h.start >= pathPrefixLength) {
|
||||
labelHighlights.push({ start: h.start - pathPrefixLength, end: h.end - pathPrefixLength });
|
||||
}
|
||||
|
||||
// Match on description part
|
||||
else {
|
||||
descriptionHighlights.push(h);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
labelHighlights = candidateLabelHighlights;
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight only inside label
|
||||
else {
|
||||
labelHighlights = filters.matchesFuzzy(lookFor, label);
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight by label otherwise
|
||||
else {
|
||||
labelHighlights = filters.matchesFuzzy(lookFor, label);
|
||||
}
|
||||
|
||||
return { labelHighlights, descriptionHighlights };
|
||||
}
|
||||
|
||||
public isFile(): boolean {
|
||||
return false; // TODO@Ben debt with editor history merging
|
||||
}
|
||||
}
|
||||
|
||||
export class QuickOpenEntryGroup extends QuickOpenEntry {
|
||||
private entry: QuickOpenEntry;
|
||||
private groupLabel: string;
|
||||
private withBorder: boolean;
|
||||
|
||||
constructor(entry?: QuickOpenEntry, groupLabel?: string, withBorder?: boolean) {
|
||||
super();
|
||||
|
||||
this.entry = entry;
|
||||
this.groupLabel = groupLabel;
|
||||
this.withBorder = withBorder;
|
||||
}
|
||||
|
||||
/**
|
||||
* The label of the group or null if none.
|
||||
*/
|
||||
public getGroupLabel(): string {
|
||||
return this.groupLabel;
|
||||
}
|
||||
|
||||
public setGroupLabel(groupLabel: string): void {
|
||||
this.groupLabel = groupLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to show a border on top of the group entry or not.
|
||||
*/
|
||||
public showBorder(): boolean {
|
||||
return this.withBorder;
|
||||
}
|
||||
|
||||
public setShowBorder(showBorder: boolean): void {
|
||||
this.withBorder = showBorder;
|
||||
}
|
||||
|
||||
public getLabel(): string {
|
||||
return this.entry ? this.entry.getLabel() : super.getLabel();
|
||||
}
|
||||
|
||||
public getLabelOptions(): IIconLabelOptions {
|
||||
return this.entry ? this.entry.getLabelOptions() : super.getLabelOptions();
|
||||
}
|
||||
|
||||
public getAriaLabel(): string {
|
||||
return this.entry ? this.entry.getAriaLabel() : super.getAriaLabel();
|
||||
}
|
||||
|
||||
public getDetail(): string {
|
||||
return this.entry ? this.entry.getDetail() : super.getDetail();
|
||||
}
|
||||
|
||||
public getResource(): URI {
|
||||
return this.entry ? this.entry.getResource() : super.getResource();
|
||||
}
|
||||
|
||||
public getIcon(): string {
|
||||
return this.entry ? this.entry.getIcon() : super.getIcon();
|
||||
}
|
||||
|
||||
public getDescription(): string {
|
||||
return this.entry ? this.entry.getDescription() : super.getDescription();
|
||||
}
|
||||
|
||||
public getEntry(): QuickOpenEntry {
|
||||
return this.entry;
|
||||
}
|
||||
|
||||
public getHighlights(): [IHighlight[], IHighlight[], IHighlight[]] {
|
||||
return this.entry ? this.entry.getHighlights() : super.getHighlights();
|
||||
}
|
||||
|
||||
public isHidden(): boolean {
|
||||
return this.entry ? this.entry.isHidden() : super.isHidden();
|
||||
}
|
||||
|
||||
public setHighlights(labelHighlights: IHighlight[], descriptionHighlights?: IHighlight[], detailHighlights?: IHighlight[]): void {
|
||||
this.entry ? this.entry.setHighlights(labelHighlights, descriptionHighlights, detailHighlights) : super.setHighlights(labelHighlights, descriptionHighlights, detailHighlights);
|
||||
}
|
||||
|
||||
public setHidden(hidden: boolean): void {
|
||||
this.entry ? this.entry.setHidden(hidden) : super.setHidden(hidden);
|
||||
}
|
||||
|
||||
public run(mode: Mode, context: IContext): boolean {
|
||||
return this.entry ? this.entry.run(mode, context) : super.run(mode, context);
|
||||
}
|
||||
}
|
||||
|
||||
class NoActionProvider implements IActionProvider {
|
||||
|
||||
public hasActions(tree: ITree, element: any): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public getActions(tree: ITree, element: any): TPromise<IAction[]> {
|
||||
return TPromise.as(null);
|
||||
}
|
||||
|
||||
public hasSecondaryActions(tree: ITree, element: any): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public getSecondaryActions(tree: ITree, element: any): TPromise<IAction[]> {
|
||||
return TPromise.as(null);
|
||||
}
|
||||
|
||||
public getActionItem(tree: ITree, element: any, action: Action): IActionItem {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IQuickOpenEntryTemplateData {
|
||||
container: HTMLElement;
|
||||
entry: HTMLElement;
|
||||
icon: HTMLSpanElement;
|
||||
label: IconLabel;
|
||||
detail: HighlightedLabel;
|
||||
description: HighlightedLabel;
|
||||
keybinding: KeybindingLabel;
|
||||
actionBar: ActionBar;
|
||||
}
|
||||
|
||||
export interface IQuickOpenEntryGroupTemplateData extends IQuickOpenEntryTemplateData {
|
||||
group: HTMLDivElement;
|
||||
}
|
||||
|
||||
const templateEntry = 'quickOpenEntry';
|
||||
const templateEntryGroup = 'quickOpenEntryGroup';
|
||||
|
||||
class Renderer implements IRenderer<QuickOpenEntry> {
|
||||
|
||||
private actionProvider: IActionProvider;
|
||||
private actionRunner: IActionRunner;
|
||||
|
||||
constructor(actionProvider: IActionProvider = new NoActionProvider(), actionRunner: IActionRunner = null) {
|
||||
this.actionProvider = actionProvider;
|
||||
this.actionRunner = actionRunner;
|
||||
}
|
||||
|
||||
public getHeight(entry: QuickOpenEntry): number {
|
||||
if (entry.getDetail()) {
|
||||
return 44;
|
||||
}
|
||||
|
||||
return 22;
|
||||
}
|
||||
|
||||
public getTemplateId(entry: QuickOpenEntry): string {
|
||||
if (entry instanceof QuickOpenEntryGroup) {
|
||||
return templateEntryGroup;
|
||||
}
|
||||
|
||||
return templateEntry;
|
||||
}
|
||||
|
||||
public renderTemplate(templateId: string, container: HTMLElement, styles: IQuickOpenStyles): IQuickOpenEntryGroupTemplateData {
|
||||
const entryContainer = document.createElement('div');
|
||||
DOM.addClass(entryContainer, 'sub-content');
|
||||
container.appendChild(entryContainer);
|
||||
|
||||
// Entry
|
||||
const row1 = DOM.$('.quick-open-row');
|
||||
const row2 = DOM.$('.quick-open-row');
|
||||
const entry = DOM.$('.quick-open-entry', null, row1, row2);
|
||||
entryContainer.appendChild(entry);
|
||||
|
||||
// Icon
|
||||
const icon = document.createElement('span');
|
||||
row1.appendChild(icon);
|
||||
|
||||
// Label
|
||||
const label = new IconLabel(row1, { supportHighlights: true });
|
||||
|
||||
// Description
|
||||
const descriptionContainer = document.createElement('span');
|
||||
row1.appendChild(descriptionContainer);
|
||||
DOM.addClass(descriptionContainer, 'quick-open-entry-description');
|
||||
const description = new HighlightedLabel(descriptionContainer);
|
||||
|
||||
// Keybinding
|
||||
const keybindingContainer = document.createElement('span');
|
||||
row1.appendChild(keybindingContainer);
|
||||
DOM.addClass(keybindingContainer, 'quick-open-entry-keybinding');
|
||||
const keybinding = new KeybindingLabel(keybindingContainer, OS);
|
||||
|
||||
// Detail
|
||||
const detailContainer = document.createElement('div');
|
||||
row2.appendChild(detailContainer);
|
||||
DOM.addClass(detailContainer, 'quick-open-entry-meta');
|
||||
const detail = new HighlightedLabel(detailContainer);
|
||||
|
||||
// Entry Group
|
||||
let group: HTMLDivElement;
|
||||
if (templateId === templateEntryGroup) {
|
||||
group = document.createElement('div');
|
||||
DOM.addClass(group, 'results-group');
|
||||
container.appendChild(group);
|
||||
}
|
||||
|
||||
// Actions
|
||||
DOM.addClass(container, 'actions');
|
||||
|
||||
const actionBarContainer = document.createElement('div');
|
||||
DOM.addClass(actionBarContainer, 'primary-action-bar');
|
||||
container.appendChild(actionBarContainer);
|
||||
|
||||
const actionBar = new ActionBar(actionBarContainer, {
|
||||
actionRunner: this.actionRunner
|
||||
});
|
||||
|
||||
return {
|
||||
container,
|
||||
entry,
|
||||
icon,
|
||||
label,
|
||||
detail,
|
||||
description,
|
||||
keybinding,
|
||||
group,
|
||||
actionBar
|
||||
};
|
||||
}
|
||||
|
||||
public renderElement(entry: QuickOpenEntry, templateId: string, templateData: any, styles: IQuickOpenStyles): void {
|
||||
const data: IQuickOpenEntryTemplateData = templateData;
|
||||
|
||||
// Action Bar
|
||||
if (this.actionProvider.hasActions(null, entry)) {
|
||||
DOM.addClass(data.container, 'has-actions');
|
||||
} else {
|
||||
DOM.removeClass(data.container, 'has-actions');
|
||||
}
|
||||
|
||||
data.actionBar.context = entry; // make sure the context is the current element
|
||||
|
||||
this.actionProvider.getActions(null, entry).then((actions) => {
|
||||
// TODO@Ben this will not work anymore as soon as quick open has more actions
|
||||
// but as long as there is only one are ok
|
||||
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()) {
|
||||
DOM.addClass(data.container, 'has-group-label');
|
||||
} else {
|
||||
DOM.removeClass(data.container, 'has-group-label');
|
||||
}
|
||||
|
||||
// Entry group
|
||||
if (entry instanceof QuickOpenEntryGroup) {
|
||||
const group = <QuickOpenEntryGroup>entry;
|
||||
const groupData = <IQuickOpenEntryGroupTemplateData>templateData;
|
||||
|
||||
// Border
|
||||
if (group.showBorder()) {
|
||||
DOM.addClass(groupData.container, 'results-group-separator');
|
||||
groupData.container.style.borderTopColor = styles.pickerGroupBorder.toString();
|
||||
} else {
|
||||
DOM.removeClass(groupData.container, 'results-group-separator');
|
||||
groupData.container.style.borderTopColor = null;
|
||||
}
|
||||
|
||||
// Group Label
|
||||
const groupLabel = group.getGroupLabel() || '';
|
||||
groupData.group.textContent = groupLabel;
|
||||
groupData.group.style.color = styles.pickerGroupForeground.toString();
|
||||
}
|
||||
|
||||
// Normal Entry
|
||||
if (entry instanceof QuickOpenEntry) {
|
||||
const [labelHighlights, descriptionHighlights, detailHighlights] = entry.getHighlights();
|
||||
|
||||
// Icon
|
||||
const iconClass = entry.getIcon() ? ('quick-open-entry-icon ' + entry.getIcon()) : '';
|
||||
data.icon.className = iconClass;
|
||||
|
||||
// Label
|
||||
const options: IIconLabelOptions = entry.getLabelOptions() || Object.create(null);
|
||||
options.matches = labelHighlights || [];
|
||||
data.label.setValue(entry.getLabel(), null, options);
|
||||
|
||||
// Meta
|
||||
data.detail.set(entry.getDetail(), detailHighlights);
|
||||
|
||||
// Description
|
||||
data.description.set(entry.getDescription(), descriptionHighlights || []);
|
||||
data.description.element.title = entry.getDescription();
|
||||
|
||||
// Keybinding
|
||||
data.keybinding.set(entry.getKeybinding(), null);
|
||||
}
|
||||
}
|
||||
|
||||
public disposeTemplate(templateId: string, templateData: any): void {
|
||||
const data = templateData as IQuickOpenEntryGroupTemplateData;
|
||||
data.actionBar.dispose();
|
||||
data.actionBar = null;
|
||||
data.container = null;
|
||||
data.entry = null;
|
||||
data.description.dispose();
|
||||
data.description = null;
|
||||
data.keybinding.dispose();
|
||||
data.keybinding = null;
|
||||
data.detail.dispose();
|
||||
data.detail = null;
|
||||
data.group = null;
|
||||
data.icon = null;
|
||||
data.label.dispose();
|
||||
data.label = null;
|
||||
}
|
||||
}
|
||||
|
||||
export class QuickOpenModel implements
|
||||
IModel<QuickOpenEntry>,
|
||||
IDataSource<QuickOpenEntry>,
|
||||
IFilter<QuickOpenEntry>,
|
||||
IRunner<QuickOpenEntry>
|
||||
{
|
||||
private _entries: QuickOpenEntry[];
|
||||
private _dataSource: IDataSource<QuickOpenEntry>;
|
||||
private _renderer: IRenderer<QuickOpenEntry>;
|
||||
private _filter: IFilter<QuickOpenEntry>;
|
||||
private _runner: IRunner<QuickOpenEntry>;
|
||||
private _accessibilityProvider: IAccessiblityProvider<QuickOpenEntry>;
|
||||
|
||||
constructor(entries: QuickOpenEntry[] = [], actionProvider: IActionProvider = new NoActionProvider()) {
|
||||
this._entries = entries;
|
||||
this._dataSource = this;
|
||||
this._renderer = new Renderer(actionProvider);
|
||||
this._filter = this;
|
||||
this._runner = this;
|
||||
this._accessibilityProvider = this;
|
||||
}
|
||||
|
||||
public get entries() { return this._entries; }
|
||||
public get dataSource() { return this._dataSource; }
|
||||
public get renderer() { return this._renderer; }
|
||||
public get filter() { return this._filter; }
|
||||
public get runner() { return this._runner; }
|
||||
public get accessibilityProvider() { return this._accessibilityProvider; }
|
||||
|
||||
public set entries(entries: QuickOpenEntry[]) {
|
||||
this._entries = entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds entries that should show up in the quick open viewer.
|
||||
*/
|
||||
public addEntries(entries: QuickOpenEntry[]): void {
|
||||
if (types.isArray(entries)) {
|
||||
this._entries = this._entries.concat(entries);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the entries that should show up in the quick open viewer.
|
||||
*/
|
||||
public setEntries(entries: QuickOpenEntry[]): void {
|
||||
if (types.isArray(entries)) {
|
||||
this._entries = entries;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entries that should show up in the quick open viewer.
|
||||
*
|
||||
* @visibleOnly optional parameter to only return visible entries
|
||||
*/
|
||||
public getEntries(visibleOnly?: boolean): QuickOpenEntry[] {
|
||||
if (visibleOnly) {
|
||||
return this._entries.filter((e) => !e.isHidden());
|
||||
}
|
||||
|
||||
return this._entries;
|
||||
}
|
||||
|
||||
public getId(entry: QuickOpenEntry): string {
|
||||
return entry.getId();
|
||||
}
|
||||
|
||||
public getLabel(entry: QuickOpenEntry): string {
|
||||
return entry.getLabel();
|
||||
}
|
||||
|
||||
public getAriaLabel(entry: QuickOpenEntry): string {
|
||||
const ariaLabel = entry.getAriaLabel();
|
||||
if (ariaLabel) {
|
||||
return nls.localize('quickOpenAriaLabelEntry', "{0}, picker", entry.getAriaLabel());
|
||||
}
|
||||
|
||||
return nls.localize('quickOpenAriaLabel', "picker");
|
||||
}
|
||||
|
||||
public isVisible(entry: QuickOpenEntry): boolean {
|
||||
return !entry.isHidden();
|
||||
}
|
||||
|
||||
public run(entry: QuickOpenEntry, mode: Mode, context: IContext): boolean {
|
||||
return entry.run(mode, context);
|
||||
}
|
||||
}
|
||||
120
src/vs/base/parts/quickopen/browser/quickOpenViewer.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { isFunction } from 'vs/base/common/types';
|
||||
import { ITree, IRenderer, IFilter, IDataSource, IAccessibilityProvider } from 'vs/base/parts/tree/browser/tree';
|
||||
import { IModel } from 'vs/base/parts/quickopen/common/quickOpen';
|
||||
import { IQuickOpenStyles } from 'vs/base/parts/quickopen/browser/quickOpenWidget';
|
||||
|
||||
export interface IModelProvider {
|
||||
getModel<T>(): IModel<T>;
|
||||
}
|
||||
|
||||
export class DataSource implements IDataSource {
|
||||
|
||||
private modelProvider: IModelProvider;
|
||||
|
||||
constructor(model: IModel<any>);
|
||||
constructor(modelProvider: IModelProvider);
|
||||
constructor(arg: any) {
|
||||
this.modelProvider = isFunction(arg.getModel) ? arg : { getModel: () => arg };
|
||||
}
|
||||
|
||||
public getId(tree: ITree, element: any): string {
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const model = this.modelProvider.getModel();
|
||||
return model === element ? '__root__' : model.dataSource.getId(element);
|
||||
}
|
||||
|
||||
public hasChildren(tree: ITree, element: any): boolean {
|
||||
const model = this.modelProvider.getModel();
|
||||
return model && model === element && model.entries.length > 0;
|
||||
}
|
||||
|
||||
public getChildren(tree: ITree, element: any): TPromise<any[]> {
|
||||
const model = this.modelProvider.getModel();
|
||||
return TPromise.as(model === element ? model.entries : []);
|
||||
}
|
||||
|
||||
public getParent(tree: ITree, element: any): TPromise<any> {
|
||||
return TPromise.as(null);
|
||||
}
|
||||
}
|
||||
|
||||
export class AccessibilityProvider implements IAccessibilityProvider {
|
||||
constructor(private modelProvider: IModelProvider) { }
|
||||
|
||||
public getAriaLabel(tree: ITree, element: any): string {
|
||||
const model = this.modelProvider.getModel();
|
||||
|
||||
return model.accessibilityProvider && model.accessibilityProvider.getAriaLabel(element);
|
||||
}
|
||||
|
||||
public getPosInSet(tree: ITree, element: any): string {
|
||||
const model = this.modelProvider.getModel();
|
||||
return String(model.entries.indexOf(element) + 1);
|
||||
}
|
||||
|
||||
public getSetSize(): string {
|
||||
const model = this.modelProvider.getModel();
|
||||
return String(model.entries.length);
|
||||
}
|
||||
}
|
||||
|
||||
export class Filter implements IFilter {
|
||||
|
||||
constructor(private modelProvider: IModelProvider) { }
|
||||
|
||||
public isVisible(tree: ITree, element: any): boolean {
|
||||
const model = this.modelProvider.getModel();
|
||||
|
||||
if (!model.filter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return model.filter.isVisible(element);
|
||||
}
|
||||
}
|
||||
|
||||
export class Renderer implements IRenderer {
|
||||
private styles: IQuickOpenStyles;
|
||||
|
||||
constructor(private modelProvider: IModelProvider, styles: IQuickOpenStyles) {
|
||||
this.styles = styles;
|
||||
}
|
||||
|
||||
public updateStyles(styles: IQuickOpenStyles): void {
|
||||
this.styles = styles;
|
||||
}
|
||||
|
||||
public getHeight(tree: ITree, element: any): number {
|
||||
const model = this.modelProvider.getModel();
|
||||
return model.renderer.getHeight(element);
|
||||
}
|
||||
|
||||
public getTemplateId(tree: ITree, element: any): string {
|
||||
const model = this.modelProvider.getModel();
|
||||
return model.renderer.getTemplateId(element);
|
||||
}
|
||||
|
||||
public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any {
|
||||
const model = this.modelProvider.getModel();
|
||||
return model.renderer.renderTemplate(templateId, container, this.styles);
|
||||
}
|
||||
|
||||
public renderElement(tree: ITree, element: any, templateId: string, templateData: any): void {
|
||||
const model = this.modelProvider.getModel();
|
||||
model.renderer.renderElement(element, templateId, templateData, this.styles);
|
||||
}
|
||||
|
||||
public disposeTemplate(tree: ITree, templateId: string, templateData: any): void {
|
||||
const model = this.modelProvider.getModel();
|
||||
model.renderer.disposeTemplate(templateId, templateData);
|
||||
}
|
||||
}
|
||||
993
src/vs/base/parts/quickopen/browser/quickOpenWidget.ts
Normal file
@@ -0,0 +1,993 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 nls = require('vs/nls');
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import platform = require('vs/base/common/platform');
|
||||
import { EventType } from 'vs/base/common/events';
|
||||
import types = require('vs/base/common/types');
|
||||
import errors = require('vs/base/common/errors');
|
||||
import { IQuickNavigateConfiguration, IAutoFocus, IEntryRunContext, IModel, Mode } from 'vs/base/parts/quickopen/common/quickOpen';
|
||||
import { Filter, Renderer, DataSource, IModelProvider, AccessibilityProvider } from 'vs/base/parts/quickopen/browser/quickOpenViewer';
|
||||
import { Dimension, Builder, $ } from 'vs/base/browser/builder';
|
||||
import { ISelectionEvent, IFocusEvent, ITree, ContextMenuEvent, IActionProvider, ITreeStyles } 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';
|
||||
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
|
||||
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { DefaultController, ClickBehavior } from 'vs/base/parts/tree/browser/treeDefaults';
|
||||
import DOM = require('vs/base/browser/dom');
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { IDisposable, dispose } 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';
|
||||
|
||||
export interface IQuickOpenCallbacks {
|
||||
onOk: () => void;
|
||||
onCancel: () => void;
|
||||
onType: (value: string) => void;
|
||||
onShow?: () => void;
|
||||
onHide?: (reason: HideReason) => void;
|
||||
onFocusLost?: () => boolean /* veto close */;
|
||||
}
|
||||
|
||||
export interface IQuickOpenOptions extends IQuickOpenStyles {
|
||||
minItemsToShow?: number;
|
||||
maxItemsToShow?: number;
|
||||
inputPlaceHolder: string;
|
||||
inputAriaLabel?: string;
|
||||
actionProvider?: IActionProvider;
|
||||
keyboardSupport?: boolean;
|
||||
}
|
||||
|
||||
export interface IQuickOpenStyles extends IInputBoxStyles, ITreeStyles {
|
||||
background?: Color;
|
||||
foreground?: Color;
|
||||
borderColor?: Color;
|
||||
pickerGroupForeground?: Color;
|
||||
pickerGroupBorder?: Color;
|
||||
widgetShadow?: Color;
|
||||
progressBarBackground?: Color;
|
||||
}
|
||||
|
||||
export interface IShowOptions {
|
||||
quickNavigateConfiguration?: IQuickNavigateConfiguration;
|
||||
autoFocus?: IAutoFocus;
|
||||
inputSelection?: IRange;
|
||||
}
|
||||
|
||||
export interface IQuickOpenUsageLogger {
|
||||
publicLog(eventName: string, data?: any): void;
|
||||
}
|
||||
|
||||
export class QuickOpenController extends DefaultController {
|
||||
|
||||
public onContextMenu(tree: ITree, element: any, event: ContextMenuEvent): boolean {
|
||||
if (platform.isMacintosh) {
|
||||
return this.onLeftClick(tree, element, event); // https://github.com/Microsoft/vscode/issues/1011
|
||||
}
|
||||
|
||||
return super.onContextMenu(tree, element, event);
|
||||
}
|
||||
}
|
||||
|
||||
export enum HideReason {
|
||||
ELEMENT_SELECTED,
|
||||
FOCUS_LOST,
|
||||
CANCELED
|
||||
}
|
||||
|
||||
const defaultStyles = {
|
||||
background: Color.fromHex('#1E1E1E'),
|
||||
foreground: Color.fromHex('#CCCCCC'),
|
||||
pickerGroupForeground: Color.fromHex('#0097FB'),
|
||||
pickerGroupBorder: Color.fromHex('#3F3F46'),
|
||||
widgetShadow: Color.fromHex('#000000'),
|
||||
progressBarBackground: Color.fromHex('#0E70C0')
|
||||
};
|
||||
|
||||
const DEFAULT_INPUT_ARIA_LABEL = nls.localize('quickOpenAriaLabel', "Quick picker. Type to narrow down results.");
|
||||
|
||||
export class QuickOpenWidget implements IModelProvider {
|
||||
|
||||
private static MAX_WIDTH = 600; // Max total width of quick open widget
|
||||
private static MAX_ITEMS_HEIGHT = 20 * 22; // Max height of item list below input field
|
||||
|
||||
private isDisposed: boolean;
|
||||
private options: IQuickOpenOptions;
|
||||
private builder: Builder;
|
||||
private tree: ITree;
|
||||
private inputBox: InputBox;
|
||||
private inputContainer: Builder;
|
||||
private helpText: Builder;
|
||||
private treeContainer: Builder;
|
||||
private progressBar: ProgressBar;
|
||||
private visible: boolean;
|
||||
private isLoosingFocus: boolean;
|
||||
private callbacks: IQuickOpenCallbacks;
|
||||
private toUnbind: IDisposable[];
|
||||
private quickNavigateConfiguration: IQuickNavigateConfiguration;
|
||||
private container: HTMLElement;
|
||||
private treeElement: HTMLElement;
|
||||
private inputElement: HTMLElement;
|
||||
private usageLogger: IQuickOpenUsageLogger;
|
||||
private layoutDimensions: Dimension;
|
||||
private model: IModel<any>;
|
||||
private inputChangingTimeoutHandle: number;
|
||||
private styles: IQuickOpenStyles;
|
||||
private renderer: Renderer;
|
||||
|
||||
constructor(container: HTMLElement, callbacks: IQuickOpenCallbacks, options: IQuickOpenOptions, usageLogger?: IQuickOpenUsageLogger) {
|
||||
this.isDisposed = false;
|
||||
this.toUnbind = [];
|
||||
this.container = container;
|
||||
this.callbacks = callbacks;
|
||||
this.options = options;
|
||||
this.styles = options || Object.create(null);
|
||||
mixin(this.styles, defaultStyles, false);
|
||||
this.usageLogger = usageLogger;
|
||||
this.model = null;
|
||||
}
|
||||
|
||||
public getElement(): Builder {
|
||||
return $(this.builder);
|
||||
}
|
||||
|
||||
public getModel(): IModel<any> {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
public setCallbacks(callbacks: IQuickOpenCallbacks): void {
|
||||
this.callbacks = callbacks;
|
||||
}
|
||||
|
||||
public create(): HTMLElement {
|
||||
this.builder = $().div((div: Builder) => {
|
||||
|
||||
// Eventing
|
||||
div.on(DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
|
||||
const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
|
||||
if (keyboardEvent.keyCode === KeyCode.Escape) {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
|
||||
this.hide(HideReason.CANCELED);
|
||||
}
|
||||
})
|
||||
.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: Event) => this.gainingFocus(), null, true)
|
||||
.on(DOM.EventType.BLUR, (e: Event) => this.loosingFocus(e), null, true);
|
||||
|
||||
// Progress Bar
|
||||
this.progressBar = new ProgressBar(div.clone(), { progressBarBackground: this.styles.progressBarBackground });
|
||||
this.progressBar.getContainer().hide();
|
||||
|
||||
// Input Field
|
||||
div.div({ 'class': 'quick-open-input' }, (inputContainer) => {
|
||||
this.inputContainer = inputContainer;
|
||||
this.inputBox = 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
|
||||
});
|
||||
|
||||
// ARIA
|
||||
this.inputElement = this.inputBox.inputElement;
|
||||
this.inputElement.setAttribute('role', 'combobox');
|
||||
this.inputElement.setAttribute('aria-haspopup', 'false');
|
||||
this.inputElement.setAttribute('aria-autocomplete', 'list');
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
DOM.addDisposableListener(this.inputBox.inputElement, DOM.EventType.INPUT, (e: Event) => {
|
||||
this.onType();
|
||||
});
|
||||
});
|
||||
|
||||
// Tree
|
||||
this.treeContainer = div.div({
|
||||
'class': 'quick-open-tree'
|
||||
}, (div: Builder) => {
|
||||
this.tree = new Tree(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,
|
||||
ariaLabel: nls.localize('treeAriaLabel', "Quick Picker"),
|
||||
keyboardSupport: this.options.keyboardSupport,
|
||||
preventRootFocus: true
|
||||
});
|
||||
|
||||
this.treeElement = this.tree.getHTMLElement();
|
||||
|
||||
// Handle Focus and Selection event
|
||||
this.toUnbind.push(this.tree.addListener(EventType.FOCUS, (event: IFocusEvent) => {
|
||||
this.elementFocused(event.focus, event);
|
||||
}));
|
||||
|
||||
this.toUnbind.push(this.tree.addListener(EventType.SELECTION, (event: ISelectionEvent) => {
|
||||
if (event.selection && event.selection.length > 0) {
|
||||
this.elementSelected(event.selection[0], event);
|
||||
}
|
||||
}));
|
||||
}).
|
||||
on(DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
|
||||
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);
|
||||
}
|
||||
}).
|
||||
on(DOM.EventType.KEY_UP, (e: KeyboardEvent) => {
|
||||
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
|
||||
}
|
||||
|
||||
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('quick-open-widget')
|
||||
.build(this.container);
|
||||
|
||||
// Support layout
|
||||
if (this.layoutDimensions) {
|
||||
this.layout(this.layoutDimensions);
|
||||
}
|
||||
|
||||
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) => {
|
||||
const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
|
||||
// Only handle when not in quick navigation mode
|
||||
if (this.quickNavigateConfiguration) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
this.inputBox.inputElement.focus();
|
||||
}
|
||||
});
|
||||
return this.builder.getHTMLElement();
|
||||
}
|
||||
|
||||
public style(styles: IQuickOpenStyles): void {
|
||||
this.styles = styles;
|
||||
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
protected applyStyles(): void {
|
||||
if (this.builder) {
|
||||
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);
|
||||
}
|
||||
|
||||
if (this.progressBar) {
|
||||
this.progressBar.style({
|
||||
progressBarBackground: this.styles.progressBarBackground
|
||||
});
|
||||
}
|
||||
|
||||
if (this.inputBox) {
|
||||
this.inputBox.style({
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
if (this.tree) {
|
||||
this.tree.style(this.styles);
|
||||
}
|
||||
|
||||
if (this.renderer) {
|
||||
this.renderer.updateStyles(this.styles);
|
||||
}
|
||||
}
|
||||
|
||||
private shouldOpenInBackground(e: StandardKeyboardEvent): boolean {
|
||||
if (e.keyCode !== KeyCode.RightArrow) {
|
||||
return false; // only for right arrow
|
||||
}
|
||||
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
||||
return false; // no modifiers allowed
|
||||
}
|
||||
|
||||
// validate the cursor is at the end of the input and there is no selection,
|
||||
// and if not prevent opening in the background such as the selection can be changed
|
||||
const element = this.inputBox.inputElement;
|
||||
return element.selectionEnd === this.inputBox.value.length && element.selectionStart === element.selectionEnd;
|
||||
}
|
||||
|
||||
private onType(): void {
|
||||
const value = this.inputBox.value;
|
||||
|
||||
// Adjust help text as needed if present
|
||||
if (this.helpText) {
|
||||
if (value) {
|
||||
this.helpText.hide();
|
||||
} else {
|
||||
this.helpText.show();
|
||||
}
|
||||
}
|
||||
|
||||
// Send to callbacks
|
||||
this.callbacks.onType(value);
|
||||
}
|
||||
|
||||
public navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration): void {
|
||||
if (this.isVisible()) {
|
||||
|
||||
// Transition into quick navigate mode if not yet done
|
||||
if (!this.quickNavigateConfiguration && quickNavigate) {
|
||||
this.quickNavigateConfiguration = quickNavigate;
|
||||
this.tree.DOMFocus();
|
||||
}
|
||||
|
||||
// Navigate
|
||||
this.navigateInTree(next ? KeyCode.DownArrow : KeyCode.UpArrow);
|
||||
}
|
||||
}
|
||||
|
||||
private navigateInTree(keyCode: KeyCode, isShift?: boolean): void {
|
||||
const model: IModel<any> = this.tree.getInput();
|
||||
const entries = model ? model.entries : [];
|
||||
const oldFocus = this.tree.getFocus();
|
||||
|
||||
// Normal Navigation
|
||||
switch (keyCode) {
|
||||
case KeyCode.DownArrow:
|
||||
this.tree.focusNext();
|
||||
break;
|
||||
|
||||
case KeyCode.UpArrow:
|
||||
this.tree.focusPrevious();
|
||||
break;
|
||||
|
||||
case KeyCode.PageDown:
|
||||
this.tree.focusNextPage();
|
||||
break;
|
||||
|
||||
case KeyCode.PageUp:
|
||||
this.tree.focusPreviousPage();
|
||||
break;
|
||||
|
||||
case KeyCode.Tab:
|
||||
if (isShift) {
|
||||
this.tree.focusPrevious();
|
||||
} else {
|
||||
this.tree.focusNext();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
let newFocus = this.tree.getFocus();
|
||||
|
||||
// Support cycle-through navigation if focus did not change
|
||||
if (entries.length > 1 && oldFocus === newFocus) {
|
||||
|
||||
// Up from no entry or first entry goes down to last
|
||||
if (keyCode === KeyCode.UpArrow || (keyCode === KeyCode.Tab && isShift)) {
|
||||
this.tree.focusLast();
|
||||
}
|
||||
|
||||
// Down from last entry goes to up to first
|
||||
else if (keyCode === KeyCode.DownArrow || keyCode === KeyCode.Tab && !isShift) {
|
||||
this.tree.focusFirst();
|
||||
}
|
||||
}
|
||||
|
||||
// Reveal
|
||||
newFocus = this.tree.getFocus();
|
||||
if (newFocus) {
|
||||
this.tree.reveal(newFocus).done(null, errors.onUnexpectedError);
|
||||
}
|
||||
}
|
||||
|
||||
private elementFocused(value: any, event?: any): void {
|
||||
if (!value || !this.isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ARIA
|
||||
this.inputElement.setAttribute('aria-activedescendant', this.treeElement.getAttribute('aria-activedescendant'));
|
||||
|
||||
const context: IEntryRunContext = { event: event, keymods: this.extractKeyMods(event), quickNavigateConfiguration: this.quickNavigateConfiguration };
|
||||
this.model.runner.run(value, Mode.PREVIEW, context);
|
||||
}
|
||||
|
||||
private elementSelected(value: any, event?: any, preferredMode?: Mode): void {
|
||||
let hide = true;
|
||||
|
||||
// Trigger open of element on selection
|
||||
if (this.isVisible()) {
|
||||
let mode = preferredMode || Mode.OPEN;
|
||||
|
||||
const context: IEntryRunContext = { event, keymods: this.extractKeyMods(event), quickNavigateConfiguration: this.quickNavigateConfiguration };
|
||||
|
||||
hide = this.model.runner.run(value, mode, context);
|
||||
}
|
||||
|
||||
// add telemetry when an item is accepted, logging the index of the item in the list and the length of the list
|
||||
// to measure the rate of the success and the relevance of the order
|
||||
if (this.usageLogger) {
|
||||
const indexOfAcceptedElement = this.model.entries.indexOf(value);
|
||||
const entriesCount = this.model.entries.length;
|
||||
this.usageLogger.publicLog('quickOpenWidgetItemAccepted', { index: indexOfAcceptedElement, count: entriesCount, isQuickNavigate: this.quickNavigateConfiguration ? true : false });
|
||||
}
|
||||
|
||||
// Hide if command was run successfully
|
||||
if (hide) {
|
||||
this.hide(HideReason.ELEMENT_SELECTED);
|
||||
}
|
||||
}
|
||||
|
||||
private extractKeyMods(event: any): number[] {
|
||||
const isCtrlCmd = event && (event.ctrlKey || event.metaKey || (event.payload && event.payload.originalEvent && (event.payload.originalEvent.ctrlKey || event.payload.originalEvent.metaKey)));
|
||||
|
||||
return isCtrlCmd ? [KeyMod.CtrlCmd] : [];
|
||||
}
|
||||
|
||||
public show(prefix: string, options?: IShowOptions): void;
|
||||
public show(input: IModel<any>, options?: IShowOptions): void;
|
||||
public show(param: any, options?: IShowOptions): void {
|
||||
this.visible = true;
|
||||
this.isLoosingFocus = false;
|
||||
this.quickNavigateConfiguration = options ? options.quickNavigateConfiguration : void 0;
|
||||
|
||||
// Adjust UI for quick navigate mode
|
||||
if (this.quickNavigateConfiguration) {
|
||||
this.inputContainer.hide();
|
||||
this.builder.show();
|
||||
this.tree.DOMFocus();
|
||||
}
|
||||
|
||||
// Otherwise use normal UI
|
||||
else {
|
||||
this.inputContainer.show();
|
||||
this.builder.show();
|
||||
this.inputBox.focus();
|
||||
}
|
||||
|
||||
// Adjust Help text for IE
|
||||
if (this.helpText) {
|
||||
if (this.quickNavigateConfiguration || types.isString(param)) {
|
||||
this.helpText.hide();
|
||||
} else {
|
||||
this.helpText.show();
|
||||
}
|
||||
}
|
||||
|
||||
// Show based on param
|
||||
if (types.isString(param)) {
|
||||
this.doShowWithPrefix(param);
|
||||
} else {
|
||||
this.doShowWithInput(param, options && options.autoFocus ? options.autoFocus : {});
|
||||
}
|
||||
|
||||
// Respect selectAll option
|
||||
if (options && options.inputSelection && !this.quickNavigateConfiguration) {
|
||||
this.inputBox.select(options.inputSelection);
|
||||
}
|
||||
|
||||
if (this.callbacks.onShow) {
|
||||
this.callbacks.onShow();
|
||||
}
|
||||
}
|
||||
|
||||
private doShowWithPrefix(prefix: string): void {
|
||||
this.inputBox.value = prefix;
|
||||
this.callbacks.onType(prefix);
|
||||
}
|
||||
|
||||
private doShowWithInput(input: IModel<any>, autoFocus: IAutoFocus): void {
|
||||
this.setInput(input, autoFocus);
|
||||
}
|
||||
|
||||
private setInputAndLayout(input: IModel<any>, autoFocus: IAutoFocus): void {
|
||||
this.treeContainer.style({ height: `${this.getHeight(input)}px` });
|
||||
|
||||
this.tree.setInput(null).then(() => {
|
||||
this.model = input;
|
||||
|
||||
// ARIA
|
||||
this.inputElement.setAttribute('aria-haspopup', String(input && input.entries && input.entries.length > 0));
|
||||
|
||||
return this.tree.setInput(input);
|
||||
}).done(() => {
|
||||
|
||||
// Indicate entries to tree
|
||||
this.tree.layout();
|
||||
|
||||
// Handle auto focus
|
||||
if (input && input.entries.some(e => this.isElementVisible(input, e))) {
|
||||
this.autoFocus(input, autoFocus);
|
||||
}
|
||||
}, errors.onUnexpectedError);
|
||||
}
|
||||
|
||||
private isElementVisible<T>(input: IModel<T>, e: T): boolean {
|
||||
if (!input.filter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return input.filter.isVisible(e);
|
||||
}
|
||||
|
||||
private autoFocus(input: IModel<any>, autoFocus: IAutoFocus = {}): void {
|
||||
const entries = input.entries.filter(e => this.isElementVisible(input, e));
|
||||
|
||||
// First check for auto focus of prefix matches
|
||||
if (autoFocus.autoFocusPrefixMatch) {
|
||||
let caseSensitiveMatch: any;
|
||||
let caseInsensitiveMatch: any;
|
||||
const prefix = autoFocus.autoFocusPrefixMatch;
|
||||
const lowerCasePrefix = prefix.toLowerCase();
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i];
|
||||
const label = input.dataSource.getLabel(entry);
|
||||
|
||||
if (!caseSensitiveMatch && label.indexOf(prefix) === 0) {
|
||||
caseSensitiveMatch = entry;
|
||||
} else if (!caseInsensitiveMatch && label.toLowerCase().indexOf(lowerCasePrefix) === 0) {
|
||||
caseInsensitiveMatch = entry;
|
||||
}
|
||||
|
||||
if (caseSensitiveMatch && caseInsensitiveMatch) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const entryToFocus = caseSensitiveMatch || caseInsensitiveMatch;
|
||||
if (entryToFocus) {
|
||||
this.tree.setFocus(entryToFocus);
|
||||
this.tree.reveal(entryToFocus, 0.5).done(null, errors.onUnexpectedError);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Second check for auto focus of first entry
|
||||
if (autoFocus.autoFocusFirstEntry) {
|
||||
this.tree.focusFirst();
|
||||
this.tree.reveal(this.tree.getFocus()).done(null, errors.onUnexpectedError);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for auto focus of second entry
|
||||
else if (autoFocus.autoFocusSecondEntry) {
|
||||
if (entries.length > 1) {
|
||||
this.tree.focusNth(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Finally check for auto focus of last entry
|
||||
else if (autoFocus.autoFocusLastEntry) {
|
||||
if (entries.length > 1) {
|
||||
this.tree.focusLast();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public refresh(input?: IModel<any>, autoFocus?: IAutoFocus): void {
|
||||
if (!this.isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!input) {
|
||||
input = this.tree.getInput();
|
||||
}
|
||||
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply height & Refresh
|
||||
this.treeContainer.style({ height: `${this.getHeight(input)}px` });
|
||||
this.tree.refresh().done(() => {
|
||||
|
||||
// Indicate entries to tree
|
||||
this.tree.layout();
|
||||
|
||||
// Handle auto focus
|
||||
if (autoFocus) {
|
||||
let doAutoFocus = autoFocus && input && input.entries.some(e => this.isElementVisible(input, e));
|
||||
if (doAutoFocus) {
|
||||
this.autoFocus(input, autoFocus);
|
||||
}
|
||||
}
|
||||
}, errors.onUnexpectedError);
|
||||
}
|
||||
|
||||
private getHeight(input: IModel<any>): number {
|
||||
const renderer = input.renderer;
|
||||
|
||||
if (!input) {
|
||||
const itemHeight = renderer.getHeight(null);
|
||||
|
||||
return this.options.minItemsToShow ? this.options.minItemsToShow * itemHeight : 0;
|
||||
}
|
||||
|
||||
let height = 0;
|
||||
|
||||
let preferredItemsHeight: number;
|
||||
if (this.layoutDimensions && this.layoutDimensions.height) {
|
||||
preferredItemsHeight = (this.layoutDimensions.height - 50 /* subtract height of input field (30px) and some spacing (drop shadow) to fit */) * 0.40 /* max 40% of screen */;
|
||||
}
|
||||
|
||||
if (!preferredItemsHeight || preferredItemsHeight > QuickOpenWidget.MAX_ITEMS_HEIGHT) {
|
||||
preferredItemsHeight = QuickOpenWidget.MAX_ITEMS_HEIGHT;
|
||||
}
|
||||
|
||||
const entries = input.entries.filter(e => this.isElementVisible(input, e));
|
||||
const maxEntries = this.options.maxItemsToShow || entries.length;
|
||||
for (let i = 0; i < maxEntries && i < entries.length; i++) {
|
||||
const entryHeight = renderer.getHeight(entries[i]);
|
||||
if (height + entryHeight <= preferredItemsHeight) {
|
||||
height += entryHeight;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
public hide(reason?: HideReason): void {
|
||||
if (!this.isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.visible = false;
|
||||
this.builder.hide();
|
||||
this.builder.domBlur();
|
||||
|
||||
// report failure cases
|
||||
if (reason === HideReason.CANCELED) {
|
||||
if (this.model) {
|
||||
const entriesCount = this.model.entries.filter(e => this.isElementVisible(this.model, e)).length;
|
||||
if (this.usageLogger) {
|
||||
this.usageLogger.publicLog('quickOpenWidgetCancelled', { count: entriesCount, isQuickNavigate: this.quickNavigateConfiguration ? true : false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear input field and clear tree
|
||||
this.inputBox.value = '';
|
||||
this.tree.setInput(null);
|
||||
|
||||
// ARIA
|
||||
this.inputElement.setAttribute('aria-haspopup', 'false');
|
||||
|
||||
// Reset Tree Height
|
||||
this.treeContainer.style({ height: (this.options.minItemsToShow ? this.options.minItemsToShow * 22 : 0) + 'px' });
|
||||
|
||||
// Clear any running Progress
|
||||
this.progressBar.stop().getContainer().hide();
|
||||
|
||||
// Clear Focus
|
||||
if (this.tree.isDOMFocused()) {
|
||||
this.tree.DOMBlur();
|
||||
} else if (this.inputBox.hasFocus()) {
|
||||
this.inputBox.blur();
|
||||
}
|
||||
|
||||
// Callbacks
|
||||
if (reason === HideReason.ELEMENT_SELECTED) {
|
||||
this.callbacks.onOk();
|
||||
} else {
|
||||
this.callbacks.onCancel();
|
||||
}
|
||||
|
||||
if (this.callbacks.onHide) {
|
||||
this.callbacks.onHide(reason);
|
||||
}
|
||||
}
|
||||
|
||||
public getQuickNavigateConfiguration(): IQuickNavigateConfiguration {
|
||||
return this.quickNavigateConfiguration;
|
||||
}
|
||||
|
||||
public setPlaceHolder(placeHolder: string): void {
|
||||
if (this.inputBox) {
|
||||
this.inputBox.setPlaceHolder(placeHolder);
|
||||
}
|
||||
}
|
||||
|
||||
public setValue(value: string, selection?: [number, number]): void {
|
||||
if (this.inputBox) {
|
||||
this.inputBox.value = value;
|
||||
if (Array.isArray(selection)) {
|
||||
const [start, end] = selection;
|
||||
this.inputBox.select({ start, end });
|
||||
} else {
|
||||
this.inputBox.select();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public setPassword(isPassword: boolean): void {
|
||||
if (this.inputBox) {
|
||||
this.inputBox.inputElement.type = isPassword ? 'password' : 'text';
|
||||
}
|
||||
}
|
||||
|
||||
public setInput(input: IModel<any>, autoFocus: IAutoFocus, ariaLabel?: string): void {
|
||||
if (!this.isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the input changes, indicate this to the tree
|
||||
if (!!this.getInput()) {
|
||||
this.onInputChanging();
|
||||
}
|
||||
|
||||
// Adapt tree height to entries and apply input
|
||||
this.setInputAndLayout(input, autoFocus);
|
||||
|
||||
// Apply ARIA
|
||||
if (this.inputBox) {
|
||||
this.inputBox.setAriaLabel(ariaLabel || DEFAULT_INPUT_ARIA_LABEL);
|
||||
}
|
||||
}
|
||||
|
||||
private onInputChanging(): void {
|
||||
if (this.inputChangingTimeoutHandle) {
|
||||
clearTimeout(this.inputChangingTimeoutHandle);
|
||||
this.inputChangingTimeoutHandle = null;
|
||||
}
|
||||
|
||||
// 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');
|
||||
this.inputChangingTimeoutHandle = setTimeout(() => {
|
||||
this.builder.removeClass('content-changing');
|
||||
}, 500);
|
||||
}
|
||||
|
||||
public getInput(): IModel<any> {
|
||||
return this.tree.getInput();
|
||||
}
|
||||
|
||||
public getTree(): ITree {
|
||||
return this.tree;
|
||||
}
|
||||
|
||||
public showInputDecoration(decoration: Severity): void {
|
||||
if (this.inputBox) {
|
||||
this.inputBox.showMessage({ type: decoration === Severity.Info ? MessageType.INFO : decoration === Severity.Warning ? MessageType.WARNING : MessageType.ERROR, content: '' });
|
||||
}
|
||||
}
|
||||
|
||||
public clearInputDecoration(): void {
|
||||
if (this.inputBox) {
|
||||
this.inputBox.hideMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
if (this.isVisible() && this.inputBox) {
|
||||
this.inputBox.focus();
|
||||
}
|
||||
}
|
||||
|
||||
public accept(): void {
|
||||
if (this.isVisible()) {
|
||||
const focus = this.tree.getFocus();
|
||||
if (focus) {
|
||||
this.elementSelected(focus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getProgressBar(): ProgressBar {
|
||||
return this.progressBar;
|
||||
}
|
||||
|
||||
public getInputBox(): InputBox {
|
||||
return this.inputBox;
|
||||
}
|
||||
|
||||
public setExtraClass(clazz: string): void {
|
||||
const previousClass = this.builder.getProperty('extra-class');
|
||||
if (previousClass) {
|
||||
this.builder.removeClass(previousClass);
|
||||
}
|
||||
|
||||
if (clazz) {
|
||||
this.builder.addClass(clazz);
|
||||
this.builder.setProperty('extra-class', clazz);
|
||||
} else if (previousClass) {
|
||||
this.builder.removeProperty('extra-class');
|
||||
}
|
||||
}
|
||||
|
||||
public isVisible(): boolean {
|
||||
return this.visible;
|
||||
}
|
||||
|
||||
public layout(dimension: Dimension): void {
|
||||
this.layoutDimensions = dimension;
|
||||
|
||||
// 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) {
|
||||
|
||||
// quick open
|
||||
this.builder.style({
|
||||
width: quickOpenWidth + 'px',
|
||||
marginLeft: '-' + (quickOpenWidth / 2) + 'px'
|
||||
});
|
||||
|
||||
// input field
|
||||
this.inputContainer.style({
|
||||
width: (quickOpenWidth - 12) + 'px'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private gainingFocus(): void {
|
||||
this.isLoosingFocus = false;
|
||||
}
|
||||
|
||||
private loosingFocus(e: Event): void {
|
||||
if (!this.isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relatedTarget = (<any>e).relatedTarget;
|
||||
if (!this.quickNavigateConfiguration && DOM.isAncestor(relatedTarget, this.builder.getHTMLElement())) {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
const veto = this.callbacks.onFocusLost && this.callbacks.onFocusLost();
|
||||
if (!veto) {
|
||||
this.hide(HideReason.FOCUS_LOST);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.isDisposed = true;
|
||||
this.toUnbind = dispose(this.toUnbind);
|
||||
|
||||
this.progressBar.dispose();
|
||||
this.inputBox.dispose();
|
||||
this.tree.dispose();
|
||||
}
|
||||
}
|
||||
163
src/vs/base/parts/quickopen/browser/quickopen.css
Normal file
@@ -0,0 +1,163 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.quick-open-widget {
|
||||
position: absolute;
|
||||
width: 600px;
|
||||
z-index: 2000;
|
||||
padding-bottom: 6px;
|
||||
left: 50%;
|
||||
margin-left: -300px;
|
||||
}
|
||||
|
||||
.quick-open-widget .progress-container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 38px;
|
||||
z-index: 1;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.quick-open-widget .progress-container .progress-bit {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.quick-open-widget .quick-open-input {
|
||||
width: 588px;
|
||||
border: none;
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
.quick-open-widget .quick-open-input .monaco-inputbox {
|
||||
width: 100%;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.quick-open-widget .quick-open-tree {
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.quick-open-widget .quick-open-tree .monaco-tree-row > .content > .sub-content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quick-open-widget.content-changing .quick-open-tree .monaco-scrollable-element .slider {
|
||||
display: none; /* scrollbar slider causes some hectic updates when input changes quickly, so hide it while quick open changes */
|
||||
}
|
||||
|
||||
.quick-open-widget .quick-open-tree .quick-open-entry {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.quick-open-widget .quick-open-tree .quick-open-entry > .quick-open-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon {
|
||||
overflow: hidden;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.quick-open-widget .quick-open-tree .quick-open-entry .monaco-highlighted-label span {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.quick-open-widget .quick-open-tree .quick-open-entry-meta {
|
||||
opacity: 0.7;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.quick-open-widget .quick-open-tree .quick-open-entry-description {
|
||||
opacity: 0.7;
|
||||
margin-left: 0.5em;
|
||||
font-size: 0.9em;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.quick-open-widget .quick-open-tree .content.has-group-label .quick-open-entry-keybinding {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.quick-open-widget .quick-open-tree .quick-open-entry-keybinding .monaco-keybinding-key {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.quick-open-widget .quick-open-tree .results-group {
|
||||
margin-right: 18px;
|
||||
}
|
||||
|
||||
.quick-open-widget .quick-open-tree .monaco-tree-row.focused > .content.has-actions > .results-group,
|
||||
.quick-open-widget .quick-open-tree .monaco-tree-row:hover:not(.highlighted) > .content.has-actions > .results-group,
|
||||
.quick-open-widget .quick-open-tree .focused .monaco-tree-row.focused > .content.has-actions > .results-group {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.quick-open-widget .quick-open-tree .results-group-separator {
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
box-sizing: border-box;
|
||||
margin-left: -11px;
|
||||
padding-left: 11px;
|
||||
}
|
||||
|
||||
/* Actions in Quick Open Items */
|
||||
|
||||
.monaco-tree .monaco-tree-row > .content.actions {
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.monaco-tree .monaco-tree-row > .content.actions > .sub-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.monaco-tree .monaco-tree-row > .content.actions .action-item {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.monaco-tree .monaco-tree-row > .content.actions > .primary-action-bar {
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.monaco-tree .monaco-tree-row > .content.actions > .primary-action-bar {
|
||||
display: none;
|
||||
padding: 0 0.8em 0 0.4em;
|
||||
}
|
||||
|
||||
.monaco-tree .monaco-tree-row.focused > .content.has-actions > .primary-action-bar {
|
||||
width: 0; /* in order to support a11y with keyboard, we use width: 0 to hide the actions, which still allows to "Tab" into the actions */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.monaco-tree .monaco-tree-row:hover:not(.highlighted) > .content.has-actions > .primary-action-bar,
|
||||
.monaco-tree.focused .monaco-tree-row.focused > .content.has-actions > .primary-action-bar,
|
||||
.monaco-tree .monaco-tree-row > .content.has-actions.more > .primary-action-bar {
|
||||
width: inherit;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.monaco-tree .monaco-tree-row > .content.actions > .primary-action-bar .action-label {
|
||||
margin-right: 0.4em;
|
||||
margin-top: 4px;
|
||||
background-repeat: no-repeat;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.quick-open-widget .quick-open-tree .monaco-highlighted-label .highlight {
|
||||
font-weight: bold;
|
||||
}
|
||||
91
src/vs/base/parts/quickopen/common/quickOpen.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
|
||||
export interface IQuickNavigateConfiguration {
|
||||
keybindings: ResolvedKeybinding[];
|
||||
}
|
||||
|
||||
export interface IAutoFocus {
|
||||
|
||||
/**
|
||||
* The index of the element to focus in the result list.
|
||||
*/
|
||||
autoFocusIndex?: number;
|
||||
|
||||
/**
|
||||
* If set to true, will automatically select the first entry from the result list.
|
||||
*/
|
||||
autoFocusFirstEntry?: boolean;
|
||||
|
||||
/**
|
||||
* If set to true, will automatically select the second entry from the result list.
|
||||
*/
|
||||
autoFocusSecondEntry?: boolean;
|
||||
|
||||
/**
|
||||
* If set to true, will automatically select the last entry from the result list.
|
||||
*/
|
||||
autoFocusLastEntry?: boolean;
|
||||
|
||||
/**
|
||||
* If set to true, will automatically select any entry whose label starts with the search
|
||||
* value. Since some entries to the top might match the query but not on the prefix, this
|
||||
* allows to select the most accurate match (matching the prefix) while still showing other
|
||||
* elements.
|
||||
*/
|
||||
autoFocusPrefixMatch?: string;
|
||||
}
|
||||
|
||||
export enum Mode {
|
||||
PREVIEW,
|
||||
OPEN,
|
||||
OPEN_IN_BACKGROUND
|
||||
}
|
||||
|
||||
export interface IEntryRunContext {
|
||||
event: any;
|
||||
keymods: number[];
|
||||
quickNavigateConfiguration: IQuickNavigateConfiguration;
|
||||
}
|
||||
|
||||
export interface IDataSource<T> {
|
||||
getId(entry: T): string;
|
||||
getLabel(entry: T): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* See vs/base/parts/tree/browser/tree.ts - IRenderer
|
||||
*/
|
||||
export interface IRenderer<T> {
|
||||
getHeight(entry: T): number;
|
||||
getTemplateId(entry: T): string;
|
||||
renderTemplate(templateId: string, container: HTMLElement, styles: any): any;
|
||||
renderElement(entry: T, templateId: string, templateData: any, styles: any): void;
|
||||
disposeTemplate(templateId: string, templateData: any): void;
|
||||
}
|
||||
|
||||
export interface IFilter<T> {
|
||||
isVisible(entry: T): boolean;
|
||||
}
|
||||
|
||||
export interface IAccessiblityProvider<T> {
|
||||
getAriaLabel(entry: T): string;
|
||||
}
|
||||
|
||||
export interface IRunner<T> {
|
||||
run(entry: T, mode: Mode, context: IEntryRunContext): boolean;
|
||||
}
|
||||
|
||||
export interface IModel<T> {
|
||||
entries: T[];
|
||||
dataSource: IDataSource<T>;
|
||||
renderer: IRenderer<T>;
|
||||
runner: IRunner<T>;
|
||||
filter?: IFilter<T>;
|
||||
accessibilityProvider?: IAccessiblityProvider<T>;
|
||||
}
|
||||
51
src/vs/base/parts/quickopen/test/browser/quickopen.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
|
||||
suite('QuickOpen', () => {
|
||||
test('QuickOpenModel', () => {
|
||||
const model = new QuickOpenModel();
|
||||
|
||||
const entry1 = new QuickOpenEntry();
|
||||
const entry2 = new QuickOpenEntry();
|
||||
const entry3 = new QuickOpenEntryGroup();
|
||||
|
||||
assert.notEqual(entry1.getId(), entry2.getId());
|
||||
assert.notEqual(entry2.getId(), entry3.getId());
|
||||
|
||||
model.addEntries([entry1, entry2, entry3]);
|
||||
assert.equal(3, model.getEntries().length);
|
||||
|
||||
model.setEntries([entry1, entry2]);
|
||||
assert.equal(2, model.getEntries().length);
|
||||
|
||||
entry1.setHidden(true);
|
||||
assert.equal(1, model.getEntries(true).length);
|
||||
assert.equal(entry2, model.getEntries(true)[0]);
|
||||
});
|
||||
|
||||
test('QuickOpenDataSource', () => {
|
||||
const model = new QuickOpenModel();
|
||||
|
||||
const entry1 = new QuickOpenEntry();
|
||||
const entry2 = new QuickOpenEntry();
|
||||
const entry3 = new QuickOpenEntryGroup();
|
||||
|
||||
model.addEntries([entry1, entry2, entry3]);
|
||||
|
||||
const ds = new DataSource(model);
|
||||
assert.equal(entry1.getId(), ds.getId(null, entry1));
|
||||
assert.equal(true, ds.hasChildren(null, model));
|
||||
assert.equal(false, ds.hasChildren(null, entry1));
|
||||
|
||||
ds.getChildren(null, model).then((children: any[]) => {
|
||||
assert.equal(3, children.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
1
src/vs/base/parts/tree/browser/CollapseAll.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-1 0 16 16" enable-background="new -1 0 16 16"><path fill="#424242" d="M14 1v9h-1v-8h-8v-1h9zm-11 2v1h8v8h1v-9h-9zm7 2v9h-9v-9h9zm-2 2h-5v5h5v-5z"/><rect x="4" y="9" fill="#00539C" width="3" height="1"/></svg>
|
||||
|
After Width: | Height: | Size: 281 B |
1
src/vs/base/parts/tree/browser/CollapseAll_inverse.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-1 0 16 16" enable-background="new -1 0 16 16"><path fill="#C5C5C5" d="M14 1v9h-1v-8h-8v-1h9zm-11 2v1h8v8h1v-9h-9zm7 2v9h-9v-9h9zm-2 2h-5v5h5v-5z"/><rect x="4" y="9" fill="#75BEFF" width="3" height="1"/></svg>
|
||||
|
After Width: | Height: | Size: 281 B |
1
src/vs/base/parts/tree/browser/collapsed-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#E8E8E8" d="M6 4v8l4-4-4-4zm1 2.414L8.586 8 7 9.586V6.414z"/></svg>
|
||||
|
After Width: | Height: | Size: 139 B |
1
src/vs/base/parts/tree/browser/collapsed-hc.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#fff" d="M6 4v8l4-4-4-4zm1 2.414l1.586 1.586-1.586 1.586v-3.172z"/></svg>
|
||||
|
After Width: | Height: | Size: 148 B |
1
src/vs/base/parts/tree/browser/collapsed.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#646465" d="M6 4v8l4-4-4-4zm1 2.414L8.586 8 7 9.586V6.414z"/></svg>
|
||||
|
After Width: | Height: | Size: 139 B |
1
src/vs/base/parts/tree/browser/expanded-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#E8E8E8" d="M11 10H5.344L11 4.414V10z"/></svg>
|
||||
|
After Width: | Height: | Size: 118 B |
1
src/vs/base/parts/tree/browser/expanded-hc.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#fff" d="M11 10.07h-5.656l5.656-5.656v5.656z"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
1
src/vs/base/parts/tree/browser/expanded.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#646465" d="M11 10H5.344L11 4.414V10z"/></svg>
|
||||
|
After Width: | Height: | Size: 118 B |
31
src/vs/base/parts/tree/browser/loading-dark.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version='1.0' standalone='no' ?>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
|
||||
<style>
|
||||
circle {
|
||||
animation: ball 0.6s linear infinite;
|
||||
}
|
||||
|
||||
circle:nth-child(2) { animation-delay: 0.075s; }
|
||||
circle:nth-child(3) { animation-delay: 0.15s; }
|
||||
circle:nth-child(4) { animation-delay: 0.225s; }
|
||||
circle:nth-child(5) { animation-delay: 0.3s; }
|
||||
circle:nth-child(6) { animation-delay: 0.375s; }
|
||||
circle:nth-child(7) { animation-delay: 0.45s; }
|
||||
circle:nth-child(8) { animation-delay: 0.525s; }
|
||||
|
||||
@keyframes ball {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0.3; }
|
||||
}
|
||||
</style>
|
||||
<g style="fill:grey;">
|
||||
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
|
||||
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
|
||||
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
|
||||
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
|
||||
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
31
src/vs/base/parts/tree/browser/loading-hc.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version='1.0' standalone='no' ?>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
|
||||
<style>
|
||||
circle {
|
||||
animation: ball 0.6s linear infinite;
|
||||
}
|
||||
|
||||
circle:nth-child(2) { animation-delay: 0.075s; }
|
||||
circle:nth-child(3) { animation-delay: 0.15s; }
|
||||
circle:nth-child(4) { animation-delay: 0.225s; }
|
||||
circle:nth-child(5) { animation-delay: 0.3s; }
|
||||
circle:nth-child(6) { animation-delay: 0.375s; }
|
||||
circle:nth-child(7) { animation-delay: 0.45s; }
|
||||
circle:nth-child(8) { animation-delay: 0.525s; }
|
||||
|
||||
@keyframes ball {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0.3; }
|
||||
}
|
||||
</style>
|
||||
<g style="fill:white;">
|
||||
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
|
||||
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
|
||||
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
|
||||
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
|
||||
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
31
src/vs/base/parts/tree/browser/loading.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version='1.0' standalone='no' ?>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
|
||||
<style>
|
||||
circle {
|
||||
animation: ball 0.6s linear infinite;
|
||||
}
|
||||
|
||||
circle:nth-child(2) { animation-delay: 0.075s; }
|
||||
circle:nth-child(3) { animation-delay: 0.15s; }
|
||||
circle:nth-child(4) { animation-delay: 0.225s; }
|
||||
circle:nth-child(5) { animation-delay: 0.3s; }
|
||||
circle:nth-child(6) { animation-delay: 0.375s; }
|
||||
circle:nth-child(7) { animation-delay: 0.45s; }
|
||||
circle:nth-child(8) { animation-delay: 0.525s; }
|
||||
|
||||
@keyframes ball {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0.3; }
|
||||
}
|
||||
</style>
|
||||
<g>
|
||||
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
|
||||
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
|
||||
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
|
||||
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
|
||||
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
121
src/vs/base/parts/tree/browser/tree.css
Normal file
@@ -0,0 +1,121 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
.monaco-tree {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: -moz-none;
|
||||
-ms-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.monaco-tree > .monaco-scrollable-element {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-tree > .monaco-scrollable-element > .monaco-tree-wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.monaco-tree .monaco-tree-rows {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-tree .monaco-tree-rows > .monaco-tree-row {
|
||||
-moz-box-sizing: border-box;
|
||||
-o-box-sizing: border-box;
|
||||
-ms-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-tree-drag-image {
|
||||
display: inline-block;
|
||||
padding: 1px 7px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* for OS X ballistic scrolling */
|
||||
.monaco-tree .monaco-tree-rows > .monaco-tree-row.scrolling {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Expansion */
|
||||
|
||||
.monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.has-children > .content:before {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
display: block;
|
||||
background: url('collapsed.svg') 50% 50% no-repeat;
|
||||
width: 16px;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: -16px;
|
||||
}
|
||||
|
||||
.monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.expanded > .content:before {
|
||||
background-image: url('expanded.svg');
|
||||
}
|
||||
|
||||
.monaco-tree .monaco-tree-rows > .monaco-tree-row.has-children.loading > .content:before {
|
||||
background-image: url('loading.svg');
|
||||
}
|
||||
|
||||
/* Highlighted */
|
||||
|
||||
.monaco-tree.highlighted .monaco-tree-rows > .monaco-tree-row:not(.highlighted) {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.has-children > .content:before {
|
||||
background-image: url('collapsed-dark.svg');
|
||||
}
|
||||
|
||||
.vs-dark .monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.expanded > .content:before {
|
||||
background-image: url('expanded-dark.svg');
|
||||
}
|
||||
|
||||
.vs-dark .monaco-tree .monaco-tree-rows > .monaco-tree-row.has-children.loading > .content:before {
|
||||
background-image: url('loading-dark.svg');
|
||||
}
|
||||
|
||||
.hc-black .monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.has-children > .content:before {
|
||||
background-image: url('collapsed-hc.svg');
|
||||
}
|
||||
|
||||
.hc-black .monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.expanded > .content:before {
|
||||
background-image: url('expanded-hc.svg');
|
||||
}
|
||||
|
||||
.hc-black .monaco-tree .monaco-tree-rows > .monaco-tree-row.has-children.loading > .content:before {
|
||||
background-image: url('loading-hc.svg');
|
||||
}
|
||||
|
||||
.monaco-tree-action.collapse-all {
|
||||
background: url('CollapseAll.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.hc-black .monaco-tree-action.collapse-all,
|
||||
.vs-dark .monaco-tree-action.collapse-all {
|
||||
background: url('CollapseAll_inverse.svg') center center no-repeat;
|
||||
}
|
||||
730
src/vs/base/parts/tree/browser/tree.ts
Normal file
@@ -0,0 +1,730 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 WinJS = require('vs/base/common/winjs.base');
|
||||
import Touch = require('vs/base/browser/touch');
|
||||
import Events = require('vs/base/common/eventEmitter');
|
||||
import Mouse = require('vs/base/browser/mouseEvent');
|
||||
import Keyboard = require('vs/base/browser/keyboardEvent');
|
||||
import { INavigator } from 'vs/base/common/iterator';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import Event from 'vs/base/common/event';
|
||||
import { IAction, IActionItem } from 'vs/base/common/actions';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
|
||||
export interface ITree extends Events.IEventEmitter {
|
||||
|
||||
emit(eventType: string, data?: any): void;
|
||||
|
||||
onDOMFocus: Event<void>;
|
||||
onDOMBlur: Event<void>;
|
||||
onHighlightChange: Event<void>;
|
||||
onDispose: Event<void>;
|
||||
|
||||
/**
|
||||
* Returns the tree's DOM element.
|
||||
*/
|
||||
getHTMLElement(): HTMLElement;
|
||||
|
||||
/**
|
||||
* Lays out the tree.
|
||||
* Provide a specific height to save an (expensive) height computation.
|
||||
*/
|
||||
layout(height?: number): void;
|
||||
|
||||
/**
|
||||
* Notifies the tree that is has become visible.
|
||||
*/
|
||||
onVisible(): void;
|
||||
|
||||
/**
|
||||
* Notifies the tree that is has become hidden.
|
||||
*/
|
||||
onHidden(): void;
|
||||
|
||||
/**
|
||||
* Sets the input of the tree.
|
||||
*/
|
||||
setInput(element: any): WinJS.Promise;
|
||||
|
||||
/**
|
||||
* Returns the tree's input.
|
||||
*/
|
||||
getInput(): any;
|
||||
|
||||
/**
|
||||
* Sets DOM focus on the tree.
|
||||
*/
|
||||
DOMFocus(): void;
|
||||
|
||||
/**
|
||||
* Returns whether the tree has DOM focus.
|
||||
*/
|
||||
isDOMFocused(): boolean;
|
||||
|
||||
/**
|
||||
* Removes DOM focus from the tree.
|
||||
*/
|
||||
DOMBlur(): void;
|
||||
|
||||
/**
|
||||
* Refreshes an element.
|
||||
* Provide no arguments and it will refresh the input element.
|
||||
*/
|
||||
refresh(element?: any, recursive?: boolean): WinJS.Promise;
|
||||
|
||||
/**
|
||||
* Expands an element.
|
||||
* The returned promise returns a boolean for whether the element was expanded or not.
|
||||
*/
|
||||
expand(element: any): WinJS.Promise;
|
||||
|
||||
/**
|
||||
* Expands several elements.
|
||||
* The returned promise returns a boolean array for whether the elements were expanded or not.
|
||||
*/
|
||||
expandAll(elements?: any[]): WinJS.Promise;
|
||||
|
||||
/**
|
||||
* Collapses an element.
|
||||
* The returned promise returns a boolean for whether the element was collapsed or not.
|
||||
*/
|
||||
collapse(element: any, recursive?: boolean): WinJS.Promise;
|
||||
|
||||
/**
|
||||
* Collapses several elements.
|
||||
* Provide no arguments and it will recursively collapse all elements in the tree
|
||||
* The returned promise returns a boolean for whether the elements were collapsed or not.
|
||||
*/
|
||||
collapseAll(elements?: any[], recursive?: boolean): WinJS.Promise;
|
||||
|
||||
/**
|
||||
* Toggles an element's expansion state.
|
||||
*/
|
||||
toggleExpansion(element: any, recursive?: boolean): WinJS.Promise;
|
||||
|
||||
/**
|
||||
* Toggles several element's expansion state.
|
||||
*/
|
||||
toggleExpansionAll(elements: any[]): WinJS.Promise;
|
||||
|
||||
/**
|
||||
* Returns whether an element is expanded or not.
|
||||
*/
|
||||
isExpanded(element: any): boolean;
|
||||
|
||||
/**
|
||||
* Returns a list of the currently expanded elements.
|
||||
*/
|
||||
getExpandedElements(): any[];
|
||||
|
||||
/**
|
||||
* Reveals an element in the tree. The relativeTop is a value between 0 and 1. The closer to 0 the more the
|
||||
* element will scroll up to the top.
|
||||
*/
|
||||
reveal(element: any, relativeTop?: number): WinJS.Promise;
|
||||
|
||||
/**
|
||||
* Returns the relative top position of any given element, if visible.
|
||||
* If not visible, returns a negative number or a number > 1.
|
||||
* Useful when calling `reveal(element, relativeTop)`.
|
||||
*/
|
||||
getRelativeTop(element: any): number;
|
||||
|
||||
/**
|
||||
* Returns a number between 0 and 1 representing how much the tree is scroll down. 0 means all the way
|
||||
* to the top; 1 means all the way down.
|
||||
*/
|
||||
getScrollPosition(): number;
|
||||
|
||||
/**
|
||||
* Sets the scroll position with a number between 0 and 1 representing how much the tree is scroll down. 0 means all the way
|
||||
* to the top; 1 means all the way down.
|
||||
*/
|
||||
setScrollPosition(pos: number): void;
|
||||
|
||||
/**
|
||||
* Returns the total height of the tree's content.
|
||||
*/
|
||||
getContentHeight(): number;
|
||||
|
||||
/**
|
||||
* Sets the tree's highlight to be the given element.
|
||||
* Provide no arguments and it clears the tree's highlight.
|
||||
*/
|
||||
setHighlight(element?: any, eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Returns the currently highlighted element.
|
||||
*/
|
||||
getHighlight(includeHidden?: boolean): any;
|
||||
|
||||
/**
|
||||
* Returns whether an element is highlighted or not.
|
||||
*/
|
||||
isHighlighted(element: any): boolean;
|
||||
|
||||
/**
|
||||
* Clears the highlight.
|
||||
*/
|
||||
clearHighlight(eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Selects an element.
|
||||
*/
|
||||
select(element: any, eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Selects a range of elements.
|
||||
*/
|
||||
selectRange(fromElement: any, toElement: any, eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Deselects a range of elements.
|
||||
*/
|
||||
deselectRange(fromElement: any, toElement: any, eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Selects several elements.
|
||||
*/
|
||||
selectAll(elements: any[], eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Deselects an element.
|
||||
*/
|
||||
deselect(element: any, eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Deselects several elements.
|
||||
*/
|
||||
deselectAll(elements: any[], eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Replaces the current selection with the given elements.
|
||||
*/
|
||||
setSelection(elements: any[], eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Toggles the element's selection.
|
||||
*/
|
||||
toggleSelection(element: any, eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Returns the currently selected elements.
|
||||
*/
|
||||
getSelection(includeHidden?: boolean): any[];
|
||||
|
||||
/**
|
||||
* Returns whether an element is selected or not.
|
||||
*/
|
||||
isSelected(element: any): boolean;
|
||||
|
||||
/**
|
||||
* Selects the next `count`-nth element, in visible order.
|
||||
*/
|
||||
selectNext(count?: number, clearSelection?: boolean, eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Selects the previous `count`-nth element, in visible order.
|
||||
*/
|
||||
selectPrevious(count?: number, clearSelection?: boolean, eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Selects the currently selected element's parent.
|
||||
*/
|
||||
selectParent(clearSelection?: boolean, eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Clears the selection.
|
||||
*/
|
||||
clearSelection(eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Sets the focused element.
|
||||
*/
|
||||
setFocus(element?: any, eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Returns whether an element is focused or not.
|
||||
*/
|
||||
isFocused(element: any): boolean;
|
||||
|
||||
/**
|
||||
* Returns focused element.
|
||||
*/
|
||||
getFocus(includeHidden?: boolean): any;
|
||||
|
||||
/**
|
||||
* Focuses the next `count`-nth element, in visible order.
|
||||
*/
|
||||
focusNext(count?: number, eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Focuses the previous `count`-nth element, in visible order.
|
||||
*/
|
||||
focusPrevious(count?: number, eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Focuses the currently focused element's parent.
|
||||
*/
|
||||
focusParent(eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Focuses the first child of the currently focused element.
|
||||
*/
|
||||
focusFirstChild(eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Focuses the second element, in visible order. Will focus the first
|
||||
* child from the provided element's parent if any.
|
||||
*/
|
||||
focusFirst(eventPayload?: any, from?: any): void;
|
||||
|
||||
/**
|
||||
* Focuses the nth element, in visible order.
|
||||
*/
|
||||
focusNth(index: number, eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Focuses the last element, in visible order. Will focus the last
|
||||
* child from the provided element's parent if any.
|
||||
*/
|
||||
focusLast(eventPayload?: any, from?: any): void;
|
||||
|
||||
/**
|
||||
* Focuses the element at the end of the next page, in visible order.
|
||||
*/
|
||||
focusNextPage(eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Focuses the element at the beginning of the previous page, in visible order.
|
||||
*/
|
||||
focusPreviousPage(eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Clears the focus.
|
||||
*/
|
||||
clearFocus(eventPayload?: any): void;
|
||||
|
||||
/**
|
||||
* Adds the trait to elements.
|
||||
*/
|
||||
addTraits(trait: string, elements: any[]): void;
|
||||
|
||||
/**
|
||||
* Removes the trait from elements.
|
||||
*/
|
||||
removeTraits(trait: string, elements: any[]): void;
|
||||
|
||||
/**
|
||||
* Toggles the element's trait.
|
||||
*/
|
||||
toggleTrait(trait: string, element: any): void;
|
||||
|
||||
/**
|
||||
* Returns whether the element has the trait or not.
|
||||
*/
|
||||
hasTrait(trait: string, element: any): boolean;
|
||||
|
||||
/**
|
||||
* Returns a navigator which allows to discover the visible and
|
||||
* expanded elements in the tree.
|
||||
*/
|
||||
getNavigator(fromElement?: any, subTreeOnly?: boolean): INavigator<any>;
|
||||
|
||||
/**
|
||||
* Apply styles to the tree.
|
||||
*/
|
||||
style(styles: ITreeStyles): void;
|
||||
|
||||
/**
|
||||
* Disposes the tree
|
||||
*/
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface IDataSource {
|
||||
|
||||
/**
|
||||
* Returns the unique identifier of the given element.
|
||||
* No more than one element may use a given identifier.
|
||||
*/
|
||||
getId(tree: ITree, element: any): string;
|
||||
|
||||
/**
|
||||
* Returns a boolean value indicating whether the element has children.
|
||||
*/
|
||||
hasChildren(tree: ITree, element: any): boolean;
|
||||
|
||||
/**
|
||||
* Returns the element's children as an array in a promise.
|
||||
*/
|
||||
getChildren(tree: ITree, element: any): WinJS.Promise;
|
||||
|
||||
/**
|
||||
* Returns the element's parent in a promise.
|
||||
*/
|
||||
getParent(tree: ITree, element: any): WinJS.Promise;
|
||||
|
||||
/**
|
||||
* Returns whether an element should be expanded when first added to the tree.
|
||||
*/
|
||||
shouldAutoexpand?(tree: ITree, element: any): boolean;
|
||||
}
|
||||
|
||||
export interface IRenderer {
|
||||
|
||||
/**
|
||||
* Returns the element's height in the tree, in pixels.
|
||||
*/
|
||||
getHeight(tree: ITree, element: any): number;
|
||||
|
||||
/**
|
||||
* Returns a template ID for a given element. This will be used as an identifier
|
||||
* for the next 3 methods.
|
||||
*/
|
||||
getTemplateId(tree: ITree, element: any): string;
|
||||
|
||||
/**
|
||||
* Renders the template in a DOM element. This method should render all the DOM
|
||||
* structure for an hypothetical element leaving its contents blank. It should
|
||||
* return an object bag which will be passed along to `renderElement` and used
|
||||
* to fill in those blanks.
|
||||
*
|
||||
* You should do all DOM creating and object allocation in this method. It
|
||||
* will be called only a few times.
|
||||
*/
|
||||
renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any;
|
||||
|
||||
/**
|
||||
* Renders an element, given an object bag returned by `renderTemplate`.
|
||||
* This method should do as little as possible and ideally it should only fill
|
||||
* in the blanks left by `renderTemplate`.
|
||||
*
|
||||
* Try to make this method do as little possible, since it will be called very
|
||||
* often.
|
||||
*/
|
||||
renderElement(tree: ITree, element: any, templateId: string, templateData: any): void;
|
||||
|
||||
/**
|
||||
* Disposes a template that was once rendered.
|
||||
*/
|
||||
disposeTemplate(tree: ITree, templateId: string, templateData: any): void;
|
||||
}
|
||||
|
||||
export interface IAccessibilityProvider {
|
||||
|
||||
/**
|
||||
* Given an element in the tree, return the ARIA label that should be associated with the
|
||||
* item. This helps screen readers to provide a meaningful label for the currently focused
|
||||
* tree element.
|
||||
*
|
||||
* Returning null will not disable ARIA for the element. Instead it is up to the screen reader
|
||||
* to compute a meaningful label based on the contents of the element in the DOM
|
||||
*
|
||||
* See also: https://www.w3.org/TR/wai-aria/states_and_properties#aria-label
|
||||
*/
|
||||
getAriaLabel(tree: ITree, element: any): string;
|
||||
|
||||
/**
|
||||
* Given an element in the tree return its aria-posinset. Should be between 1 and aria-setsize
|
||||
* https://www.w3.org/TR/wai-aria/states_and_properties#aria-posinset
|
||||
*/
|
||||
getPosInSet?(tree: ITree, element: any): string;
|
||||
|
||||
/**
|
||||
* Return the aria-setsize of the tree.
|
||||
* https://www.w3.org/TR/wai-aria/states_and_properties#aria-setsize
|
||||
*/
|
||||
getSetSize?(): string;
|
||||
}
|
||||
|
||||
export /* abstract */ class ContextMenuEvent {
|
||||
|
||||
private _posx: number;
|
||||
private _posy: number;
|
||||
private _target: HTMLElement;
|
||||
|
||||
constructor(posx: number, posy: number, target: HTMLElement) {
|
||||
this._posx = posx;
|
||||
this._posy = posy;
|
||||
this._target = target;
|
||||
}
|
||||
|
||||
public preventDefault(): void {
|
||||
// no-op
|
||||
}
|
||||
|
||||
public stopPropagation(): void {
|
||||
// no-op
|
||||
}
|
||||
|
||||
public get posx(): number {
|
||||
return this._posx;
|
||||
}
|
||||
|
||||
public get posy(): number {
|
||||
return this._posy;
|
||||
}
|
||||
|
||||
public get target(): HTMLElement {
|
||||
return this._target;
|
||||
}
|
||||
}
|
||||
|
||||
export class MouseContextMenuEvent extends ContextMenuEvent {
|
||||
|
||||
private originalEvent: Mouse.IMouseEvent;
|
||||
|
||||
constructor(originalEvent: Mouse.IMouseEvent) {
|
||||
super(originalEvent.posx, originalEvent.posy, originalEvent.target);
|
||||
this.originalEvent = originalEvent;
|
||||
}
|
||||
|
||||
public preventDefault(): void {
|
||||
this.originalEvent.preventDefault();
|
||||
}
|
||||
|
||||
public stopPropagation(): void {
|
||||
this.originalEvent.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
export class KeyboardContextMenuEvent extends ContextMenuEvent {
|
||||
|
||||
private originalEvent: Keyboard.IKeyboardEvent;
|
||||
|
||||
constructor(posx: number, posy: number, originalEvent: Keyboard.IKeyboardEvent) {
|
||||
super(posx, posy, originalEvent.target);
|
||||
this.originalEvent = originalEvent;
|
||||
}
|
||||
|
||||
public preventDefault(): void {
|
||||
this.originalEvent.preventDefault();
|
||||
}
|
||||
|
||||
public stopPropagation(): void {
|
||||
this.originalEvent.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
export interface IController {
|
||||
|
||||
/**
|
||||
* Called when an element is clicked.
|
||||
*/
|
||||
onClick(tree: ITree, element: any, event: Mouse.IMouseEvent): boolean;
|
||||
|
||||
/**
|
||||
* Called when an element is requested for a context menu.
|
||||
*/
|
||||
onContextMenu(tree: ITree, element: any, event: ContextMenuEvent): boolean;
|
||||
|
||||
/**
|
||||
* Called when an element is tapped.
|
||||
*/
|
||||
onTap(tree: ITree, element: any, event: Touch.GestureEvent): boolean;
|
||||
|
||||
/**
|
||||
* Called when a key is pressed down while selecting elements.
|
||||
*/
|
||||
onKeyDown(tree: ITree, event: Keyboard.IKeyboardEvent): boolean;
|
||||
|
||||
/**
|
||||
* Called when a key is released while selecting elements.
|
||||
*/
|
||||
onKeyUp(tree: ITree, event: Keyboard.IKeyboardEvent): boolean;
|
||||
|
||||
/**
|
||||
* Called when a mouse button is pressed down on an element.
|
||||
*/
|
||||
onMouseDown?(tree: ITree, element: any, event: Mouse.IMouseEvent): boolean;
|
||||
|
||||
/**
|
||||
* Called when a mouse button goes up on an element.
|
||||
*/
|
||||
onMouseUp?(tree: ITree, element: any, event: Mouse.IMouseEvent): boolean;
|
||||
}
|
||||
|
||||
export enum DragOverEffect {
|
||||
COPY,
|
||||
MOVE
|
||||
}
|
||||
|
||||
export enum DragOverBubble {
|
||||
BUBBLE_DOWN,
|
||||
BUBBLE_UP
|
||||
}
|
||||
|
||||
export interface IDragOverReaction {
|
||||
accept: boolean;
|
||||
effect?: DragOverEffect;
|
||||
bubble?: DragOverBubble;
|
||||
autoExpand?: boolean;
|
||||
}
|
||||
|
||||
export const DRAG_OVER_REJECT: IDragOverReaction = { accept: false };
|
||||
export const DRAG_OVER_ACCEPT: IDragOverReaction = { accept: true };
|
||||
export const DRAG_OVER_ACCEPT_BUBBLE_UP: IDragOverReaction = { accept: true, bubble: DragOverBubble.BUBBLE_UP };
|
||||
export const DRAG_OVER_ACCEPT_BUBBLE_DOWN = (autoExpand = false) => ({ accept: true, bubble: DragOverBubble.BUBBLE_DOWN, autoExpand });
|
||||
export const DRAG_OVER_ACCEPT_BUBBLE_UP_COPY: IDragOverReaction = { accept: true, bubble: DragOverBubble.BUBBLE_UP, effect: DragOverEffect.COPY };
|
||||
export const DRAG_OVER_ACCEPT_BUBBLE_DOWN_COPY = (autoExpand = false) => ({ accept: true, bubble: DragOverBubble.BUBBLE_DOWN, effect: DragOverEffect.COPY, autoExpand });
|
||||
|
||||
export interface IDragAndDropData {
|
||||
update(event: Mouse.DragMouseEvent): void;
|
||||
getData(): any;
|
||||
}
|
||||
|
||||
export interface IDragAndDrop {
|
||||
|
||||
/**
|
||||
* Returns a uri if the given element should be allowed to drag.
|
||||
* Returns null, otherwise.
|
||||
*/
|
||||
getDragURI(tree: ITree, element: any): string;
|
||||
|
||||
/**
|
||||
* Returns a label to display when dragging the element.
|
||||
*/
|
||||
getDragLabel?(tree: ITree, elements: any[]): string;
|
||||
|
||||
/**
|
||||
* Sent when the drag operation is starting.
|
||||
*/
|
||||
onDragStart(tree: ITree, data: IDragAndDropData, originalEvent: Mouse.DragMouseEvent): void;
|
||||
|
||||
/**
|
||||
* Returns a DragOverReaction indicating whether sources can be
|
||||
* dropped into target or some parent of the target.
|
||||
*/
|
||||
onDragOver(tree: ITree, data: IDragAndDropData, targetElement: any, originalEvent: Mouse.DragMouseEvent): IDragOverReaction;
|
||||
|
||||
/**
|
||||
* Handles the action of dropping sources into target.
|
||||
*/
|
||||
drop(tree: ITree, data: IDragAndDropData, targetElement: any, originalEvent: Mouse.DragMouseEvent): void;
|
||||
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
/**
|
||||
* Handles the action of canceled drag-and-drop activities
|
||||
*/
|
||||
dropAbort(tree: ITree, data: IDragAndDropData): void;
|
||||
|
||||
}
|
||||
|
||||
export interface IFilter {
|
||||
|
||||
/**
|
||||
* Returns whether the given element should be visible.
|
||||
*/
|
||||
isVisible(tree: ITree, element: any): boolean;
|
||||
}
|
||||
|
||||
export interface IElementCallback {
|
||||
(tree: ITree, element: any): void;
|
||||
}
|
||||
|
||||
export type ICallback = () => void;
|
||||
|
||||
export interface ISorter {
|
||||
|
||||
/**
|
||||
* Compare two elements in the viewer to define the sorting order.
|
||||
*/
|
||||
compare(tree: ITree, element: any, otherElement: any): number;
|
||||
}
|
||||
|
||||
// Events
|
||||
|
||||
export interface ISelectionEvent {
|
||||
selection: any[];
|
||||
payload?: any;
|
||||
}
|
||||
|
||||
export interface IFocusEvent {
|
||||
focus: any;
|
||||
payload?: any;
|
||||
}
|
||||
|
||||
export interface IHighlightEvent {
|
||||
highlight: any;
|
||||
payload?: any;
|
||||
}
|
||||
|
||||
// Options
|
||||
|
||||
export interface ITreeConfiguration {
|
||||
dataSource: IDataSource;
|
||||
renderer?: IRenderer;
|
||||
controller?: IController;
|
||||
dnd?: IDragAndDrop;
|
||||
filter?: IFilter;
|
||||
sorter?: ISorter;
|
||||
accessibilityProvider?: IAccessibilityProvider;
|
||||
}
|
||||
|
||||
export interface ITreeOptions extends ITreeStyles {
|
||||
twistiePixels?: number;
|
||||
showTwistie?: boolean;
|
||||
indentPixels?: number;
|
||||
verticalScrollMode?: ScrollbarVisibility;
|
||||
alwaysFocused?: boolean;
|
||||
autoExpandSingleChildren?: boolean;
|
||||
useShadows?: boolean;
|
||||
paddingOnRow?: boolean;
|
||||
ariaLabel?: string;
|
||||
keyboardSupport?: boolean;
|
||||
preventRootFocus?: boolean;
|
||||
}
|
||||
|
||||
export interface ITreeStyles {
|
||||
listFocusBackground?: Color;
|
||||
listFocusForeground?: Color;
|
||||
listActiveSelectionBackground?: Color;
|
||||
listActiveSelectionForeground?: Color;
|
||||
listFocusAndSelectionBackground?: Color;
|
||||
listFocusAndSelectionForeground?: Color;
|
||||
listInactiveSelectionBackground?: Color;
|
||||
listInactiveSelectionForeground?: Color;
|
||||
listHoverBackground?: Color;
|
||||
listHoverForeground?: Color;
|
||||
listDropBackground?: Color;
|
||||
listFocusOutline?: Color;
|
||||
}
|
||||
|
||||
export interface ITreeContext extends ITreeConfiguration {
|
||||
tree: ITree;
|
||||
options: ITreeOptions;
|
||||
}
|
||||
|
||||
export interface IActionProvider {
|
||||
|
||||
/**
|
||||
* Returns whether or not the element has actions. These show up in place right to the element in the tree.
|
||||
*/
|
||||
hasActions(tree: ITree, element: any): boolean;
|
||||
|
||||
/**
|
||||
* 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[]>;
|
||||
|
||||
/**
|
||||
* Returns whether or not the element has secondary actions. These show up once the user has expanded the element's action bar.
|
||||
*/
|
||||
hasSecondaryActions(tree: ITree, element: any): boolean;
|
||||
|
||||
/**
|
||||
* 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[]>;
|
||||
|
||||
/**
|
||||
* Returns an action item to render an action.
|
||||
*/
|
||||
getActionItem(tree: ITree, element: any, action: IAction): IActionItem;
|
||||
}
|
||||
441
src/vs/base/parts/tree/browser/treeDefaults.ts
Normal file
@@ -0,0 +1,441 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 nls = require('vs/nls');
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import platform = require('vs/base/common/platform');
|
||||
import touch = require('vs/base/browser/touch');
|
||||
import errors = require('vs/base/common/errors');
|
||||
import dom = require('vs/base/browser/dom');
|
||||
import mouse = require('vs/base/browser/mouseEvent');
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import _ = require('vs/base/parts/tree/browser/tree');
|
||||
import { KeyCode, KeyMod, Keybinding, createKeybinding, SimpleKeybinding } from 'vs/base/common/keyCodes';
|
||||
|
||||
export interface IKeyBindingCallback {
|
||||
(tree: _.ITree, event: IKeyboardEvent): void;
|
||||
}
|
||||
|
||||
export interface ICancelableEvent {
|
||||
preventDefault(): void;
|
||||
stopPropagation(): void;
|
||||
}
|
||||
|
||||
export enum ClickBehavior {
|
||||
|
||||
/**
|
||||
* Handle the click when the mouse button is pressed but not released yet.
|
||||
*/
|
||||
ON_MOUSE_DOWN,
|
||||
|
||||
/**
|
||||
* Handle the click when the mouse button is released.
|
||||
*/
|
||||
ON_MOUSE_UP
|
||||
}
|
||||
|
||||
export interface IControllerOptions {
|
||||
clickBehavior?: ClickBehavior;
|
||||
keyboardSupport?: boolean;
|
||||
}
|
||||
|
||||
interface IKeybindingDispatcherItem {
|
||||
keybinding: Keybinding;
|
||||
callback: IKeyBindingCallback;
|
||||
}
|
||||
|
||||
export class KeybindingDispatcher {
|
||||
|
||||
private _arr: IKeybindingDispatcherItem[];
|
||||
|
||||
constructor() {
|
||||
this._arr = [];
|
||||
}
|
||||
|
||||
public set(keybinding: number, callback: IKeyBindingCallback) {
|
||||
this._arr.push({
|
||||
keybinding: createKeybinding(keybinding, platform.OS),
|
||||
callback: callback
|
||||
});
|
||||
}
|
||||
|
||||
public dispatch(keybinding: SimpleKeybinding): IKeyBindingCallback {
|
||||
// Loop from the last to the first to handle overwrites
|
||||
for (let i = this._arr.length - 1; i >= 0; i--) {
|
||||
let item = this._arr[i];
|
||||
if (keybinding.equals(item.keybinding)) {
|
||||
return item.callback;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class DefaultController implements _.IController {
|
||||
|
||||
protected downKeyBindingDispatcher: KeybindingDispatcher;
|
||||
protected upKeyBindingDispatcher: KeybindingDispatcher;
|
||||
|
||||
private options: IControllerOptions;
|
||||
|
||||
constructor(options: IControllerOptions = { clickBehavior: ClickBehavior.ON_MOUSE_UP, keyboardSupport: true }) {
|
||||
this.options = options;
|
||||
|
||||
this.downKeyBindingDispatcher = new KeybindingDispatcher();
|
||||
this.upKeyBindingDispatcher = new KeybindingDispatcher();
|
||||
|
||||
if (typeof options.keyboardSupport !== 'boolean' || options.keyboardSupport) {
|
||||
this.downKeyBindingDispatcher.set(KeyCode.UpArrow, (t, e) => this.onUp(t, e));
|
||||
this.downKeyBindingDispatcher.set(KeyCode.DownArrow, (t, e) => this.onDown(t, e));
|
||||
this.downKeyBindingDispatcher.set(KeyCode.LeftArrow, (t, e) => this.onLeft(t, e));
|
||||
this.downKeyBindingDispatcher.set(KeyCode.RightArrow, (t, e) => this.onRight(t, e));
|
||||
if (platform.isMacintosh) {
|
||||
this.downKeyBindingDispatcher.set(KeyMod.CtrlCmd | KeyCode.UpArrow, (t, e) => this.onLeft(t, e));
|
||||
this.downKeyBindingDispatcher.set(KeyMod.WinCtrl | KeyCode.KEY_N, (t, e) => this.onDown(t, e));
|
||||
this.downKeyBindingDispatcher.set(KeyMod.WinCtrl | KeyCode.KEY_P, (t, e) => this.onUp(t, e));
|
||||
}
|
||||
this.downKeyBindingDispatcher.set(KeyCode.PageUp, (t, e) => this.onPageUp(t, e));
|
||||
this.downKeyBindingDispatcher.set(KeyCode.PageDown, (t, e) => this.onPageDown(t, e));
|
||||
this.downKeyBindingDispatcher.set(KeyCode.Home, (t, e) => this.onHome(t, e));
|
||||
this.downKeyBindingDispatcher.set(KeyCode.End, (t, e) => this.onEnd(t, e));
|
||||
|
||||
this.downKeyBindingDispatcher.set(KeyCode.Space, (t, e) => this.onSpace(t, e));
|
||||
this.downKeyBindingDispatcher.set(KeyCode.Escape, (t, e) => this.onEscape(t, e));
|
||||
|
||||
this.upKeyBindingDispatcher.set(KeyCode.Enter, this.onEnter.bind(this));
|
||||
this.upKeyBindingDispatcher.set(KeyMod.CtrlCmd | KeyCode.Enter, this.onEnter.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
public onMouseDown(tree: _.ITree, element: any, event: mouse.IMouseEvent, origin: string = 'mouse'): boolean {
|
||||
if (this.options.clickBehavior === ClickBehavior.ON_MOUSE_DOWN && (event.leftButton || event.middleButton)) {
|
||||
if (event.target) {
|
||||
if (event.target.tagName && event.target.tagName.toLowerCase() === 'input') {
|
||||
return false; // Ignore event if target is a form input field (avoids browser specific issues)
|
||||
}
|
||||
|
||||
if (dom.findParentWithClass(event.target, 'monaco-action-bar', 'row')) { // TODO@Joao not very nice way of checking for the action bar (implicit knowledge)
|
||||
return false; // Ignore event if target is over an action bar of the row
|
||||
}
|
||||
}
|
||||
|
||||
// Propagate to onLeftClick now
|
||||
return this.onLeftClick(tree, element, event, origin);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public onClick(tree: _.ITree, element: any, event: mouse.IMouseEvent): boolean {
|
||||
var isMac = platform.isMacintosh;
|
||||
|
||||
// A Ctrl click on the Mac is a context menu event
|
||||
if (isMac && event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.target && event.target.tagName && event.target.tagName.toLowerCase() === 'input') {
|
||||
return false; // Ignore event if target is a form input field (avoids browser specific issues)
|
||||
}
|
||||
|
||||
if (this.options.clickBehavior === ClickBehavior.ON_MOUSE_DOWN && (event.leftButton || event.middleButton)) {
|
||||
return false; // Already handled by onMouseDown
|
||||
}
|
||||
|
||||
return this.onLeftClick(tree, element, event);
|
||||
}
|
||||
|
||||
protected onLeftClick(tree: _.ITree, element: any, eventish: ICancelableEvent, origin: string = 'mouse'): boolean {
|
||||
var payload = { origin: origin, originalEvent: eventish };
|
||||
|
||||
if (tree.getInput() === element) {
|
||||
tree.clearFocus(payload);
|
||||
tree.clearSelection(payload);
|
||||
} else {
|
||||
var isMouseDown = eventish && (<mouse.IMouseEvent>eventish).browserEvent && (<mouse.IMouseEvent>eventish).browserEvent.type === 'mousedown';
|
||||
if (!isMouseDown) {
|
||||
eventish.preventDefault(); // we cannot preventDefault onMouseDown because this would break DND otherwise
|
||||
}
|
||||
eventish.stopPropagation();
|
||||
|
||||
tree.DOMFocus();
|
||||
tree.setSelection([element], payload);
|
||||
tree.setFocus(element, payload);
|
||||
|
||||
if (tree.isExpanded(element)) {
|
||||
tree.collapse(element).done(null, errors.onUnexpectedError);
|
||||
} else {
|
||||
tree.expand(element).done(null, errors.onUnexpectedError);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public onContextMenu(tree: _.ITree, element: any, event: _.ContextMenuEvent): boolean {
|
||||
if (event.target && event.target.tagName && event.target.tagName.toLowerCase() === 'input') {
|
||||
return false; // allow context menu on input fields
|
||||
}
|
||||
|
||||
// Prevent native context menu from showing up
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public onTap(tree: _.ITree, element: any, event: touch.GestureEvent): boolean {
|
||||
var target = <HTMLElement>event.initialTarget;
|
||||
|
||||
if (target && target.tagName && target.tagName.toLowerCase() === 'input') {
|
||||
return false; // Ignore event if target is a form input field (avoids browser specific issues)
|
||||
}
|
||||
|
||||
return this.onLeftClick(tree, element, event, 'touch');
|
||||
}
|
||||
|
||||
public onKeyDown(tree: _.ITree, event: IKeyboardEvent): boolean {
|
||||
return this.onKey(this.downKeyBindingDispatcher, tree, event);
|
||||
}
|
||||
|
||||
public onKeyUp(tree: _.ITree, event: IKeyboardEvent): boolean {
|
||||
return this.onKey(this.upKeyBindingDispatcher, tree, event);
|
||||
}
|
||||
|
||||
private onKey(bindings: KeybindingDispatcher, tree: _.ITree, event: IKeyboardEvent): boolean {
|
||||
var handler = bindings.dispatch(event.toKeybinding());
|
||||
if (handler) {
|
||||
if (handler(tree, event)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected onUp(tree: _.ITree, event: IKeyboardEvent): boolean {
|
||||
var payload = { origin: 'keyboard', originalEvent: event };
|
||||
|
||||
if (tree.getHighlight()) {
|
||||
tree.clearHighlight(payload);
|
||||
} else {
|
||||
tree.focusPrevious(1, payload);
|
||||
tree.reveal(tree.getFocus()).done(null, errors.onUnexpectedError);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected onPageUp(tree: _.ITree, event: IKeyboardEvent): boolean {
|
||||
var payload = { origin: 'keyboard', originalEvent: event };
|
||||
|
||||
if (tree.getHighlight()) {
|
||||
tree.clearHighlight(payload);
|
||||
} else {
|
||||
tree.focusPreviousPage(payload);
|
||||
tree.reveal(tree.getFocus()).done(null, errors.onUnexpectedError);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected onDown(tree: _.ITree, event: IKeyboardEvent): boolean {
|
||||
var payload = { origin: 'keyboard', originalEvent: event };
|
||||
|
||||
if (tree.getHighlight()) {
|
||||
tree.clearHighlight(payload);
|
||||
} else {
|
||||
tree.focusNext(1, payload);
|
||||
tree.reveal(tree.getFocus()).done(null, errors.onUnexpectedError);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected onPageDown(tree: _.ITree, event: IKeyboardEvent): boolean {
|
||||
var payload = { origin: 'keyboard', originalEvent: event };
|
||||
|
||||
if (tree.getHighlight()) {
|
||||
tree.clearHighlight(payload);
|
||||
} else {
|
||||
tree.focusNextPage(payload);
|
||||
tree.reveal(tree.getFocus()).done(null, errors.onUnexpectedError);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected onHome(tree: _.ITree, event: IKeyboardEvent): boolean {
|
||||
var payload = { origin: 'keyboard', originalEvent: event };
|
||||
|
||||
if (tree.getHighlight()) {
|
||||
tree.clearHighlight(payload);
|
||||
} else {
|
||||
tree.focusFirst(payload);
|
||||
tree.reveal(tree.getFocus()).done(null, errors.onUnexpectedError);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected onEnd(tree: _.ITree, event: IKeyboardEvent): boolean {
|
||||
var payload = { origin: 'keyboard', originalEvent: event };
|
||||
|
||||
if (tree.getHighlight()) {
|
||||
tree.clearHighlight(payload);
|
||||
} else {
|
||||
tree.focusLast(payload);
|
||||
tree.reveal(tree.getFocus()).done(null, errors.onUnexpectedError);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected onLeft(tree: _.ITree, event: IKeyboardEvent): boolean {
|
||||
var payload = { origin: 'keyboard', originalEvent: event };
|
||||
|
||||
if (tree.getHighlight()) {
|
||||
tree.clearHighlight(payload);
|
||||
} else {
|
||||
var focus = tree.getFocus();
|
||||
tree.collapse(focus).then(didCollapse => {
|
||||
if (focus && !didCollapse) {
|
||||
tree.focusParent(payload);
|
||||
return tree.reveal(tree.getFocus());
|
||||
}
|
||||
return undefined;
|
||||
}).done(null, errors.onUnexpectedError);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected onRight(tree: _.ITree, event: IKeyboardEvent): boolean {
|
||||
var payload = { origin: 'keyboard', originalEvent: event };
|
||||
|
||||
if (tree.getHighlight()) {
|
||||
tree.clearHighlight(payload);
|
||||
} else {
|
||||
var focus = tree.getFocus();
|
||||
tree.expand(focus).then(didExpand => {
|
||||
if (focus && !didExpand) {
|
||||
tree.focusFirstChild(payload);
|
||||
return tree.reveal(tree.getFocus());
|
||||
}
|
||||
return undefined;
|
||||
}).done(null, errors.onUnexpectedError);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected onEnter(tree: _.ITree, event: IKeyboardEvent): boolean {
|
||||
var payload = { origin: 'keyboard', originalEvent: event };
|
||||
|
||||
if (tree.getHighlight()) {
|
||||
return false;
|
||||
}
|
||||
var focus = tree.getFocus();
|
||||
if (focus) {
|
||||
tree.setSelection([focus], payload);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected onSpace(tree: _.ITree, event: IKeyboardEvent): boolean {
|
||||
if (tree.getHighlight()) {
|
||||
return false;
|
||||
}
|
||||
var focus = tree.getFocus();
|
||||
if (focus) {
|
||||
tree.toggleExpansion(focus);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected onEscape(tree: _.ITree, event: IKeyboardEvent): boolean {
|
||||
var payload = { origin: 'keyboard', originalEvent: event };
|
||||
|
||||
if (tree.getHighlight()) {
|
||||
tree.clearHighlight(payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tree.getSelection().length) {
|
||||
tree.clearSelection(payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tree.getFocus()) {
|
||||
tree.clearFocus(payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class DefaultDragAndDrop implements _.IDragAndDrop {
|
||||
|
||||
public getDragURI(tree: _.ITree, element: any): string {
|
||||
return null;
|
||||
}
|
||||
|
||||
public onDragStart(tree: _.ITree, data: _.IDragAndDropData, originalEvent: mouse.DragMouseEvent): void {
|
||||
return;
|
||||
}
|
||||
|
||||
public onDragOver(tree: _.ITree, data: _.IDragAndDropData, targetElement: any, originalEvent: mouse.DragMouseEvent): _.IDragOverReaction {
|
||||
return null;
|
||||
}
|
||||
|
||||
public drop(tree: _.ITree, data: _.IDragAndDropData, targetElement: any, originalEvent: mouse.DragMouseEvent): void {
|
||||
return;
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
public dropAbort(tree: _.ITree, data: _.IDragAndDropData): void { }
|
||||
}
|
||||
|
||||
export class DefaultFilter implements _.IFilter {
|
||||
|
||||
public isVisible(tree: _.ITree, element: any): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class DefaultSorter implements _.ISorter {
|
||||
|
||||
public compare(tree: _.ITree, element: any, otherElement: any): number {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class DefaultAccessibilityProvider implements _.IAccessibilityProvider {
|
||||
|
||||
getAriaLabel(tree: _.ITree, element: any): string {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class CollapseAllAction extends Action {
|
||||
|
||||
constructor(private viewer: _.ITree, enabled: boolean) {
|
||||
super('vs.tree.collapse', nls.localize('collapse', "Collapse"), 'monaco-tree-action collapse-all', enabled);
|
||||
}
|
||||
|
||||
public run(context?: any): TPromise<any> {
|
||||
if (this.viewer.getHighlight()) {
|
||||
return TPromise.as(null); // Global action disabled if user is in edit mode from another action
|
||||
}
|
||||
|
||||
this.viewer.collapseAll();
|
||||
this.viewer.clearSelection();
|
||||
this.viewer.clearFocus();
|
||||
this.viewer.DOMFocus();
|
||||
this.viewer.focusFirst();
|
||||
|
||||
return TPromise.as(null);
|
||||
}
|
||||
}
|
||||
122
src/vs/base/parts/tree/browser/treeDnd.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 _ = require('vs/base/parts/tree/browser/tree');
|
||||
import Mouse = require('vs/base/browser/mouseEvent');
|
||||
import { DefaultDragAndDrop } from 'vs/base/parts/tree/browser/treeDefaults';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { basename } from 'vs/base/common/paths';
|
||||
import { getPathLabel } from 'vs/base/common/labels';
|
||||
|
||||
export class ElementsDragAndDropData implements _.IDragAndDropData {
|
||||
|
||||
private elements: any[];
|
||||
|
||||
constructor(elements: any[]) {
|
||||
this.elements = elements;
|
||||
}
|
||||
|
||||
public update(event: Mouse.DragMouseEvent): void {
|
||||
// no-op
|
||||
}
|
||||
|
||||
public getData(): any {
|
||||
return this.elements;
|
||||
}
|
||||
}
|
||||
|
||||
export class ExternalElementsDragAndDropData implements _.IDragAndDropData {
|
||||
|
||||
private elements: any[];
|
||||
|
||||
constructor(elements: any[]) {
|
||||
this.elements = elements;
|
||||
}
|
||||
|
||||
public update(event: Mouse.DragMouseEvent): void {
|
||||
// no-op
|
||||
}
|
||||
|
||||
public getData(): any {
|
||||
return this.elements;
|
||||
}
|
||||
}
|
||||
|
||||
export class DesktopDragAndDropData implements _.IDragAndDropData {
|
||||
|
||||
private types: any[];
|
||||
private files: any[];
|
||||
|
||||
constructor() {
|
||||
this.types = [];
|
||||
this.files = [];
|
||||
}
|
||||
|
||||
public update(event: Mouse.DragMouseEvent): void {
|
||||
if (event.dataTransfer.types) {
|
||||
this.types = [];
|
||||
Array.prototype.push.apply(this.types, event.dataTransfer.types);
|
||||
}
|
||||
|
||||
if (event.dataTransfer.files) {
|
||||
this.files = [];
|
||||
Array.prototype.push.apply(this.files, event.dataTransfer.files);
|
||||
|
||||
this.files = this.files.filter(f => f.size || f.type);
|
||||
}
|
||||
}
|
||||
|
||||
public getData(): any {
|
||||
return {
|
||||
types: this.types,
|
||||
files: this.files
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class SimpleFileResourceDragAndDrop extends DefaultDragAndDrop {
|
||||
|
||||
constructor(private toResource: (obj: any) => URI) {
|
||||
super();
|
||||
}
|
||||
|
||||
public getDragURI(tree: _.ITree, obj: any): string {
|
||||
const resource = this.toResource(obj);
|
||||
if (resource) {
|
||||
return resource.toString();
|
||||
}
|
||||
|
||||
return void 0;
|
||||
}
|
||||
|
||||
public getDragLabel(tree: _.ITree, elements: any[]): string {
|
||||
if (elements.length > 1) {
|
||||
return String(elements.length);
|
||||
}
|
||||
|
||||
const resource = this.toResource(elements[0]);
|
||||
if (resource) {
|
||||
return basename(resource.fsPath);
|
||||
}
|
||||
|
||||
return void 0;
|
||||
}
|
||||
|
||||
public onDragStart(tree: _.ITree, data: _.IDragAndDropData, originalEvent: Mouse.DragMouseEvent): void {
|
||||
const sources: object[] = data.getData();
|
||||
|
||||
let source: object = null;
|
||||
if (sources.length > 0) {
|
||||
source = sources[0];
|
||||
}
|
||||
|
||||
// Apply some datatransfer types to allow for dragging the element outside of the application
|
||||
const resource = this.toResource(source);
|
||||
if (resource) {
|
||||
originalEvent.dataTransfer.setData('text/plain', getPathLabel(resource));
|
||||
}
|
||||
}
|
||||
}
|
||||
387
src/vs/base/parts/tree/browser/treeImpl.ts
Normal file
@@ -0,0 +1,387 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 WinJS = require('vs/base/common/winjs.base');
|
||||
import TreeDefaults = require('vs/base/parts/tree/browser/treeDefaults');
|
||||
import Events = require('vs/base/common/eventEmitter');
|
||||
import Model = require('vs/base/parts/tree/browser/treeModel');
|
||||
import View = require('./treeView');
|
||||
import _ = require('vs/base/parts/tree/browser/tree');
|
||||
import { INavigator, MappedNavigator } from 'vs/base/common/iterator';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
import Lifecycle = require('vs/base/common/lifecycle');
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
|
||||
export class TreeContext implements _.ITreeContext {
|
||||
|
||||
public tree: _.ITree;
|
||||
public configuration: _.ITreeConfiguration;
|
||||
public options: _.ITreeOptions;
|
||||
|
||||
public dataSource: _.IDataSource;
|
||||
public renderer: _.IRenderer;
|
||||
public controller: _.IController;
|
||||
public dnd: _.IDragAndDrop;
|
||||
public filter: _.IFilter;
|
||||
public sorter: _.ISorter;
|
||||
public accessibilityProvider: _.IAccessibilityProvider;
|
||||
|
||||
constructor(tree: _.ITree, configuration: _.ITreeConfiguration, options: _.ITreeOptions = {}) {
|
||||
this.tree = tree;
|
||||
this.configuration = configuration;
|
||||
this.options = options;
|
||||
|
||||
if (!configuration.dataSource) {
|
||||
throw new Error('You must provide a Data Source to the tree.');
|
||||
}
|
||||
|
||||
this.dataSource = configuration.dataSource;
|
||||
this.renderer = configuration.renderer;
|
||||
this.controller = configuration.controller || new TreeDefaults.DefaultController({ clickBehavior: TreeDefaults.ClickBehavior.ON_MOUSE_UP, keyboardSupport: typeof options.keyboardSupport !== 'boolean' || options.keyboardSupport });
|
||||
this.dnd = configuration.dnd || new TreeDefaults.DefaultDragAndDrop();
|
||||
this.filter = configuration.filter || new TreeDefaults.DefaultFilter();
|
||||
this.sorter = configuration.sorter || null;
|
||||
this.accessibilityProvider = configuration.accessibilityProvider || new TreeDefaults.DefaultAccessibilityProvider();
|
||||
}
|
||||
}
|
||||
|
||||
const defaultStyles: _.ITreeStyles = {
|
||||
listFocusBackground: Color.fromHex('#073655'),
|
||||
listActiveSelectionBackground: Color.fromHex('#3062D6'),
|
||||
listActiveSelectionForeground: Color.fromHex('#FFFFFF'),
|
||||
listFocusAndSelectionBackground: Color.fromHex('#094771'),
|
||||
listFocusAndSelectionForeground: Color.fromHex('#FFFFFF'),
|
||||
listInactiveSelectionBackground: Color.fromHex('#C8C8C8'),
|
||||
listHoverBackground: Color.fromHex('#DCDCDC'),
|
||||
listDropBackground: Color.fromHex('#383B3D')
|
||||
};
|
||||
|
||||
export class Tree extends Events.EventEmitter implements _.ITree {
|
||||
|
||||
private container: HTMLElement;
|
||||
private configuration: _.ITreeConfiguration;
|
||||
private options: _.ITreeOptions;
|
||||
|
||||
private context: _.ITreeContext;
|
||||
private model: Model.TreeModel;
|
||||
private view: View.TreeView;
|
||||
|
||||
private _onDispose: Emitter<void>;
|
||||
private _onHighlightChange: Emitter<void>;
|
||||
|
||||
private toDispose: Lifecycle.IDisposable[];
|
||||
|
||||
constructor(container: HTMLElement, configuration: _.ITreeConfiguration, options: _.ITreeOptions = {}) {
|
||||
super();
|
||||
|
||||
this.toDispose = [];
|
||||
|
||||
this._onDispose = new Emitter<void>();
|
||||
this._onHighlightChange = new Emitter<void>();
|
||||
|
||||
this.toDispose.push(this._onDispose, this._onHighlightChange);
|
||||
|
||||
this.container = container;
|
||||
this.configuration = configuration;
|
||||
this.options = options;
|
||||
mixin(this.options, defaultStyles, false);
|
||||
|
||||
this.options.twistiePixels = typeof this.options.twistiePixels === 'number' ? this.options.twistiePixels : 32;
|
||||
this.options.showTwistie = this.options.showTwistie === false ? false : true;
|
||||
this.options.indentPixels = typeof this.options.indentPixels === 'number' ? this.options.indentPixels : 12;
|
||||
this.options.alwaysFocused = this.options.alwaysFocused === true ? true : false;
|
||||
this.options.useShadows = this.options.useShadows === false ? false : true;
|
||||
this.options.paddingOnRow = this.options.paddingOnRow === false ? false : true;
|
||||
|
||||
this.context = new TreeContext(this, configuration, options);
|
||||
this.model = new Model.TreeModel(this.context);
|
||||
this.view = new View.TreeView(this.context, this.container);
|
||||
|
||||
this.view.setModel(this.model);
|
||||
|
||||
this.addEmitter(this.model);
|
||||
this.addEmitter(this.view);
|
||||
|
||||
this.toDispose.push(this.model.addListener('highlight', () => this._onHighlightChange.fire()));
|
||||
}
|
||||
|
||||
public style(styles: _.ITreeStyles): void {
|
||||
this.view.applyStyles(styles);
|
||||
}
|
||||
|
||||
get onDOMFocus(): Event<void> {
|
||||
return this.view && this.view.onDOMFocus;
|
||||
}
|
||||
|
||||
get onDOMBlur(): Event<void> {
|
||||
return this.view && this.view.onDOMBlur;
|
||||
}
|
||||
|
||||
get onHighlightChange(): Event<void> {
|
||||
return this._onHighlightChange && this._onHighlightChange.event;
|
||||
}
|
||||
|
||||
get onDispose(): Event<void> {
|
||||
return this._onDispose && this._onDispose.event;
|
||||
}
|
||||
|
||||
public getHTMLElement(): HTMLElement {
|
||||
return this.view.getHTMLElement();
|
||||
}
|
||||
|
||||
public layout(height?: number): void {
|
||||
this.view.layout(height);
|
||||
}
|
||||
|
||||
public DOMFocus(): void {
|
||||
this.view.focus();
|
||||
}
|
||||
|
||||
public isDOMFocused(): boolean {
|
||||
return this.view.isFocused();
|
||||
}
|
||||
|
||||
public DOMBlur(): void {
|
||||
this.view.blur();
|
||||
}
|
||||
|
||||
public onVisible(): void {
|
||||
this.view.onVisible();
|
||||
}
|
||||
|
||||
public onHidden(): void {
|
||||
this.view.onHidden();
|
||||
}
|
||||
|
||||
public setInput(element: any): WinJS.Promise {
|
||||
return this.model.setInput(element);
|
||||
}
|
||||
|
||||
public getInput(): any {
|
||||
return this.model.getInput();
|
||||
}
|
||||
|
||||
public refresh(element: any = null, recursive = true): WinJS.Promise {
|
||||
return this.model.refresh(element, recursive);
|
||||
}
|
||||
|
||||
public expand(element: any): WinJS.Promise {
|
||||
return this.model.expand(element);
|
||||
}
|
||||
|
||||
public expandAll(elements: any[]): WinJS.Promise {
|
||||
return this.model.expandAll(elements);
|
||||
}
|
||||
|
||||
public collapse(element: any, recursive: boolean = false): WinJS.Promise {
|
||||
return this.model.collapse(element, recursive);
|
||||
}
|
||||
|
||||
public collapseAll(elements: any[] = null, recursive: boolean = false): WinJS.Promise {
|
||||
return this.model.collapseAll(elements, recursive);
|
||||
}
|
||||
|
||||
public toggleExpansion(element: any, recursive: boolean = false): WinJS.Promise {
|
||||
return this.model.toggleExpansion(element, recursive);
|
||||
}
|
||||
|
||||
public toggleExpansionAll(elements: any[]): WinJS.Promise {
|
||||
return this.model.toggleExpansionAll(elements);
|
||||
}
|
||||
|
||||
public isExpanded(element: any): boolean {
|
||||
return this.model.isExpanded(element);
|
||||
}
|
||||
|
||||
public getExpandedElements(): any[] {
|
||||
return this.model.getExpandedElements();
|
||||
}
|
||||
|
||||
public reveal(element: any, relativeTop: number = null): WinJS.Promise {
|
||||
return this.model.reveal(element, relativeTop);
|
||||
}
|
||||
|
||||
public getRelativeTop(element: any): number {
|
||||
let item = this.model.getItem(element);
|
||||
return this.view.getRelativeTop(item);
|
||||
}
|
||||
|
||||
public getScrollPosition(): number {
|
||||
return this.view.getScrollPosition();
|
||||
}
|
||||
|
||||
public setScrollPosition(pos: number): void {
|
||||
this.view.setScrollPosition(pos);
|
||||
}
|
||||
|
||||
getContentHeight(): number {
|
||||
return this.view.getTotalHeight();
|
||||
}
|
||||
|
||||
public setHighlight(element?: any, eventPayload?: any): void {
|
||||
this.model.setHighlight(element, eventPayload);
|
||||
}
|
||||
|
||||
public getHighlight(): any {
|
||||
return this.model.getHighlight();
|
||||
}
|
||||
|
||||
public isHighlighted(element: any): boolean {
|
||||
return this.model.isFocused(element);
|
||||
}
|
||||
|
||||
public clearHighlight(eventPayload?: any): void {
|
||||
this.model.setHighlight(null, eventPayload);
|
||||
}
|
||||
|
||||
public select(element: any, eventPayload?: any): void {
|
||||
this.model.select(element, eventPayload);
|
||||
}
|
||||
|
||||
public selectRange(fromElement: any, toElement: any, eventPayload?: any): void {
|
||||
this.model.selectRange(fromElement, toElement, eventPayload);
|
||||
}
|
||||
|
||||
public deselectRange(fromElement: any, toElement: any, eventPayload?: any): void {
|
||||
this.model.deselectRange(fromElement, toElement, eventPayload);
|
||||
}
|
||||
|
||||
public selectAll(elements: any[], eventPayload?: any): void {
|
||||
this.model.selectAll(elements, eventPayload);
|
||||
}
|
||||
|
||||
public deselect(element: any, eventPayload?: any): void {
|
||||
this.model.deselect(element, eventPayload);
|
||||
}
|
||||
|
||||
public deselectAll(elements: any[], eventPayload?: any): void {
|
||||
this.model.deselectAll(elements, eventPayload);
|
||||
}
|
||||
|
||||
public setSelection(elements: any[], eventPayload?: any): void {
|
||||
this.model.setSelection(elements, eventPayload);
|
||||
}
|
||||
|
||||
public toggleSelection(element: any, eventPayload?: any): void {
|
||||
this.model.toggleSelection(element, eventPayload);
|
||||
}
|
||||
|
||||
public isSelected(element: any): boolean {
|
||||
return this.model.isSelected(element);
|
||||
}
|
||||
|
||||
public getSelection(): any[] {
|
||||
return this.model.getSelection();
|
||||
}
|
||||
|
||||
public clearSelection(eventPayload?: any): void {
|
||||
this.model.setSelection([], eventPayload);
|
||||
}
|
||||
|
||||
public selectNext(count?: number, clearSelection?: boolean, eventPayload?: any): void {
|
||||
this.model.selectNext(count, clearSelection, eventPayload);
|
||||
}
|
||||
|
||||
public selectPrevious(count?: number, clearSelection?: boolean, eventPayload?: any): void {
|
||||
this.model.selectPrevious(count, clearSelection, eventPayload);
|
||||
}
|
||||
|
||||
public selectParent(clearSelection?: boolean, eventPayload?: any): void {
|
||||
this.model.selectParent(clearSelection, eventPayload);
|
||||
}
|
||||
|
||||
public setFocus(element?: any, eventPayload?: any): void {
|
||||
this.model.setFocus(element, eventPayload);
|
||||
}
|
||||
|
||||
public isFocused(element: any): boolean {
|
||||
return this.model.isFocused(element);
|
||||
}
|
||||
|
||||
public getFocus(): any {
|
||||
return this.model.getFocus();
|
||||
}
|
||||
|
||||
public focusNext(count?: number, eventPayload?: any): void {
|
||||
this.model.focusNext(count, eventPayload);
|
||||
}
|
||||
|
||||
public focusPrevious(count?: number, eventPayload?: any): void {
|
||||
this.model.focusPrevious(count, eventPayload);
|
||||
}
|
||||
|
||||
public focusParent(eventPayload?: any): void {
|
||||
this.model.focusParent(eventPayload);
|
||||
}
|
||||
|
||||
public focusFirstChild(eventPayload?: any): void {
|
||||
this.model.focusFirstChild(eventPayload);
|
||||
}
|
||||
|
||||
public focusFirst(eventPayload?: any, from?: any): void {
|
||||
this.model.focusFirst(eventPayload, from);
|
||||
}
|
||||
|
||||
public focusNth(index: number, eventPayload?: any): void {
|
||||
this.model.focusNth(index, eventPayload);
|
||||
}
|
||||
|
||||
public focusLast(eventPayload?: any, from?: any): void {
|
||||
this.model.focusLast(eventPayload, from);
|
||||
}
|
||||
|
||||
public focusNextPage(eventPayload?: any): void {
|
||||
this.view.focusNextPage(eventPayload);
|
||||
}
|
||||
|
||||
public focusPreviousPage(eventPayload?: any): void {
|
||||
this.view.focusPreviousPage(eventPayload);
|
||||
}
|
||||
|
||||
public clearFocus(eventPayload?: any): void {
|
||||
this.model.setFocus(null, eventPayload);
|
||||
}
|
||||
|
||||
public addTraits(trait: string, elements: any[]): void {
|
||||
this.model.addTraits(trait, elements);
|
||||
}
|
||||
|
||||
public removeTraits(trait: string, elements: any[]): void {
|
||||
this.model.removeTraits(trait, elements);
|
||||
}
|
||||
|
||||
public toggleTrait(trait: string, element: any): void {
|
||||
this.model.hasTrait(trait, element) ? this.model.removeTraits(trait, [element])
|
||||
: this.model.addTraits(trait, [element]);
|
||||
}
|
||||
|
||||
public hasTrait(trait: string, element: any): boolean {
|
||||
return this.model.hasTrait(trait, element);
|
||||
}
|
||||
|
||||
getNavigator(fromElement?: any, subTreeOnly?: boolean): INavigator<any> {
|
||||
return new MappedNavigator(this.model.getNavigator(fromElement, subTreeOnly), i => i && i.getElement());
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._onDispose.fire();
|
||||
|
||||
if (this.model !== null) {
|
||||
this.model.dispose();
|
||||
this.model = null;
|
||||
}
|
||||
if (this.view !== null) {
|
||||
this.view.dispose();
|
||||
this.view = null;
|
||||
}
|
||||
|
||||
this.toDispose = Lifecycle.dispose(this.toDispose);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
1383
src/vs/base/parts/tree/browser/treeModel.ts
Normal file
1713
src/vs/base/parts/tree/browser/treeView.ts
Normal file
243
src/vs/base/parts/tree/browser/treeViewModel.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EventEmitter } from 'vs/base/common/eventEmitter';
|
||||
import { IIterator, ArrayIterator } from 'vs/base/common/iterator';
|
||||
import { Item } from './treeModel';
|
||||
|
||||
export interface IViewItem {
|
||||
model: Item;
|
||||
top: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export class HeightMap extends EventEmitter {
|
||||
|
||||
private heightMap: IViewItem[];
|
||||
private indexes: { [item: string]: number; };
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.heightMap = [];
|
||||
this.indexes = {};
|
||||
}
|
||||
|
||||
public getTotalHeight(): number {
|
||||
var last = this.heightMap[this.heightMap.length - 1];
|
||||
return !last ? 0 : last.top + last.height;
|
||||
}
|
||||
|
||||
public onInsertItems(iterator: IIterator<Item>, afterItemId: string = null): number {
|
||||
var item: Item;
|
||||
var viewItem: IViewItem;
|
||||
var i: number, j: number;
|
||||
var totalSize: number;
|
||||
var sizeDiff = 0;
|
||||
|
||||
if (afterItemId === null) {
|
||||
i = 0;
|
||||
totalSize = 0;
|
||||
} else {
|
||||
i = this.indexes[afterItemId] + 1;
|
||||
viewItem = this.heightMap[i - 1];
|
||||
|
||||
if (!viewItem) {
|
||||
console.error('view item doesnt exist');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
totalSize = viewItem.top + viewItem.height;
|
||||
}
|
||||
|
||||
var boundSplice = this.heightMap.splice.bind(this.heightMap, i, 0);
|
||||
|
||||
var itemsToInsert: IViewItem[] = [];
|
||||
|
||||
while (item = iterator.next()) {
|
||||
viewItem = this.createViewItem(item);
|
||||
viewItem.top = totalSize + sizeDiff;
|
||||
this.emit('viewItem:create', { item: viewItem.model });
|
||||
|
||||
this.indexes[item.id] = i++;
|
||||
itemsToInsert.push(viewItem);
|
||||
sizeDiff += viewItem.height;
|
||||
}
|
||||
|
||||
boundSplice.apply(this.heightMap, itemsToInsert);
|
||||
|
||||
for (j = i; j < this.heightMap.length; j++) {
|
||||
viewItem = this.heightMap[j];
|
||||
viewItem.top += sizeDiff;
|
||||
this.indexes[viewItem.model.id] = j;
|
||||
}
|
||||
|
||||
for (j = itemsToInsert.length - 1; j >= 0; j--) {
|
||||
this.onInsertItem(itemsToInsert[j]);
|
||||
}
|
||||
|
||||
for (j = this.heightMap.length - 1; j >= i; j--) {
|
||||
this.onRefreshItem(this.heightMap[j]);
|
||||
}
|
||||
|
||||
return sizeDiff;
|
||||
}
|
||||
|
||||
public onInsertItem(item: IViewItem): void {
|
||||
// noop
|
||||
}
|
||||
|
||||
// Contiguous items
|
||||
public onRemoveItems(iterator: IIterator<string>): void {
|
||||
var itemId: string;
|
||||
var viewItem: IViewItem;
|
||||
var startIndex: number = null;
|
||||
var i: number;
|
||||
var sizeDiff = 0;
|
||||
|
||||
while (itemId = iterator.next()) {
|
||||
i = this.indexes[itemId];
|
||||
viewItem = this.heightMap[i];
|
||||
|
||||
if (!viewItem) {
|
||||
console.error('view item doesnt exist');
|
||||
return;
|
||||
}
|
||||
|
||||
sizeDiff -= viewItem.height;
|
||||
delete this.indexes[itemId];
|
||||
this.onRemoveItem(viewItem);
|
||||
|
||||
if (startIndex === null) {
|
||||
startIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (sizeDiff === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.heightMap.splice(startIndex, i - startIndex + 1);
|
||||
|
||||
for (i = startIndex; i < this.heightMap.length; i++) {
|
||||
viewItem = this.heightMap[i];
|
||||
viewItem.top += sizeDiff;
|
||||
this.indexes[viewItem.model.id] = i;
|
||||
this.onRefreshItem(viewItem);
|
||||
}
|
||||
}
|
||||
|
||||
public onRemoveItem(item: IViewItem): void {
|
||||
// noop
|
||||
}
|
||||
|
||||
public onRefreshItemSet(items: Item[]): void {
|
||||
var sortedItems = items.sort((a, b) => this.indexes[a.id] - this.indexes[b.id]);
|
||||
this.onRefreshItems(new ArrayIterator(sortedItems));
|
||||
}
|
||||
|
||||
// Ordered, but not necessarily contiguous items
|
||||
public onRefreshItems(iterator: IIterator<Item>): void {
|
||||
var item: Item;
|
||||
var viewItem: IViewItem;
|
||||
var newHeight: number;
|
||||
var i: number, j: number = null;
|
||||
var cummDiff = 0;
|
||||
|
||||
while (item = iterator.next()) {
|
||||
i = this.indexes[item.id];
|
||||
|
||||
for (; cummDiff !== 0 && j !== null && j < i; j++) {
|
||||
viewItem = this.heightMap[j];
|
||||
viewItem.top += cummDiff;
|
||||
this.onRefreshItem(viewItem);
|
||||
}
|
||||
|
||||
viewItem = this.heightMap[i];
|
||||
newHeight = item.getHeight();
|
||||
viewItem.top += cummDiff;
|
||||
cummDiff += newHeight - viewItem.height;
|
||||
viewItem.height = newHeight;
|
||||
this.onRefreshItem(viewItem, true);
|
||||
|
||||
j = i + 1;
|
||||
}
|
||||
|
||||
if (cummDiff !== 0 && j !== null) {
|
||||
for (; j < this.heightMap.length; j++) {
|
||||
viewItem = this.heightMap[j];
|
||||
viewItem.top += cummDiff;
|
||||
this.onRefreshItem(viewItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public onRefreshItem(item: IViewItem, needsRender: boolean = false): void {
|
||||
// noop
|
||||
}
|
||||
|
||||
public itemsCount(): number {
|
||||
return this.heightMap.length;
|
||||
}
|
||||
|
||||
public itemAt(position: number): string {
|
||||
return this.heightMap[this.indexAt(position)].model.id;
|
||||
}
|
||||
|
||||
public withItemsInRange(start: number, end: number, fn: (item: string) => void): void {
|
||||
start = this.indexAt(start);
|
||||
end = this.indexAt(end);
|
||||
for (var i = start; i <= end; i++) {
|
||||
fn(this.heightMap[i].model.id);
|
||||
}
|
||||
}
|
||||
|
||||
public indexAt(position: number): number {
|
||||
var left = 0;
|
||||
var right = this.heightMap.length;
|
||||
var center: number;
|
||||
var item: IViewItem;
|
||||
|
||||
// Binary search
|
||||
while (left < right) {
|
||||
center = Math.floor((left + right) / 2);
|
||||
item = this.heightMap[center];
|
||||
|
||||
if (position < item.top) {
|
||||
right = center;
|
||||
} else if (position >= item.top + item.height) {
|
||||
if (left === center) {
|
||||
break;
|
||||
}
|
||||
left = center;
|
||||
} else {
|
||||
return center;
|
||||
}
|
||||
}
|
||||
|
||||
return this.heightMap.length;
|
||||
}
|
||||
|
||||
public indexAfter(position: number): number {
|
||||
return Math.min(this.indexAt(position) + 1, this.heightMap.length);
|
||||
}
|
||||
|
||||
public itemAtIndex(index: number): IViewItem {
|
||||
return this.heightMap[index];
|
||||
}
|
||||
|
||||
public itemAfter(item: IViewItem): IViewItem {
|
||||
return this.heightMap[this.indexes[item.model.id] + 1] || null;
|
||||
}
|
||||
|
||||
protected createViewItem(item: Item): IViewItem {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.heightMap = null;
|
||||
this.indexes = null;
|
||||
}
|
||||
}
|
||||
1736
src/vs/base/parts/tree/test/browser/treeModel.test.ts
Normal file
253
src/vs/base/parts/tree/test/browser/treeViewModel.test.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 assert = require('assert');
|
||||
import { ArrayIterator } from 'vs/base/common/iterator';
|
||||
import { HeightMap, IViewItem } from 'vs/base/parts/tree/browser/treeViewModel';
|
||||
|
||||
function makeItem(id, height): any {
|
||||
return {
|
||||
id: id,
|
||||
getHeight: function () { return height; },
|
||||
isExpanded: function () { return false; },
|
||||
getAllTraits: () => []
|
||||
};
|
||||
}
|
||||
|
||||
function makeItems(...args: any[]) {
|
||||
var r = [];
|
||||
|
||||
for (var i = 0; i < args.length; i += 2) {
|
||||
r.push(makeItem(args[i], args[i + 1]));
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
function makeNavigator(...args: any[]): any {
|
||||
var items = makeItems.apply(null, args);
|
||||
var i = 0;
|
||||
|
||||
return {
|
||||
next: function () {
|
||||
return items[i++] || null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class TestHeightMap extends HeightMap {
|
||||
|
||||
protected createViewItem(item: any): IViewItem {
|
||||
return {
|
||||
model: item,
|
||||
top: 0,
|
||||
height: item.getHeight()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
suite('TreeView - HeightMap', () => {
|
||||
var rangeMap: HeightMap;
|
||||
|
||||
setup(() => {
|
||||
rangeMap = new TestHeightMap();
|
||||
rangeMap.onInsertItems(makeNavigator('a', 3, 'b', 30, 'c', 25, 'd', 2));
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
rangeMap.dispose();
|
||||
rangeMap = null;
|
||||
});
|
||||
|
||||
test('simple', () => {
|
||||
assert.equal(rangeMap.itemAt(0), 'a');
|
||||
assert.equal(rangeMap.itemAt(2), 'a');
|
||||
assert.equal(rangeMap.itemAt(3), 'b');
|
||||
assert.equal(rangeMap.itemAt(32), 'b');
|
||||
assert.equal(rangeMap.itemAt(33), 'c');
|
||||
assert.equal(rangeMap.itemAt(40), 'c');
|
||||
assert.equal(rangeMap.itemAt(57), 'c');
|
||||
assert.equal(rangeMap.itemAt(58), 'd');
|
||||
assert.equal(rangeMap.itemAt(59), 'd');
|
||||
assert.throws(() => rangeMap.itemAt(60));
|
||||
});
|
||||
|
||||
test('onInsertItems at beginning', () => {
|
||||
var navigator = makeNavigator('x', 4, 'y', 20, 'z', 8);
|
||||
rangeMap.onInsertItems(navigator);
|
||||
|
||||
assert.equal(rangeMap.itemAt(0), 'x');
|
||||
assert.equal(rangeMap.itemAt(3), 'x');
|
||||
assert.equal(rangeMap.itemAt(4), 'y');
|
||||
assert.equal(rangeMap.itemAt(23), 'y');
|
||||
assert.equal(rangeMap.itemAt(24), 'z');
|
||||
assert.equal(rangeMap.itemAt(31), 'z');
|
||||
assert.equal(rangeMap.itemAt(32), 'a');
|
||||
assert.equal(rangeMap.itemAt(34), 'a');
|
||||
assert.equal(rangeMap.itemAt(35), 'b');
|
||||
assert.equal(rangeMap.itemAt(64), 'b');
|
||||
assert.equal(rangeMap.itemAt(65), 'c');
|
||||
assert.equal(rangeMap.itemAt(89), 'c');
|
||||
assert.equal(rangeMap.itemAt(90), 'd');
|
||||
assert.equal(rangeMap.itemAt(91), 'd');
|
||||
assert.throws(() => rangeMap.itemAt(92));
|
||||
});
|
||||
|
||||
test('onInsertItems in middle', () => {
|
||||
var navigator = makeNavigator('x', 4, 'y', 20, 'z', 8);
|
||||
rangeMap.onInsertItems(navigator, 'a');
|
||||
|
||||
assert.equal(rangeMap.itemAt(0), 'a');
|
||||
assert.equal(rangeMap.itemAt(2), 'a');
|
||||
assert.equal(rangeMap.itemAt(3), 'x');
|
||||
assert.equal(rangeMap.itemAt(6), 'x');
|
||||
assert.equal(rangeMap.itemAt(7), 'y');
|
||||
assert.equal(rangeMap.itemAt(26), 'y');
|
||||
assert.equal(rangeMap.itemAt(27), 'z');
|
||||
assert.equal(rangeMap.itemAt(34), 'z');
|
||||
assert.equal(rangeMap.itemAt(35), 'b');
|
||||
assert.equal(rangeMap.itemAt(64), 'b');
|
||||
assert.equal(rangeMap.itemAt(65), 'c');
|
||||
assert.equal(rangeMap.itemAt(89), 'c');
|
||||
assert.equal(rangeMap.itemAt(90), 'd');
|
||||
assert.equal(rangeMap.itemAt(91), 'd');
|
||||
assert.throws(() => rangeMap.itemAt(92));
|
||||
});
|
||||
|
||||
test('onInsertItems at end', () => {
|
||||
var navigator = makeNavigator('x', 4, 'y', 20, 'z', 8);
|
||||
rangeMap.onInsertItems(navigator, 'd');
|
||||
|
||||
assert.equal(rangeMap.itemAt(0), 'a');
|
||||
assert.equal(rangeMap.itemAt(2), 'a');
|
||||
assert.equal(rangeMap.itemAt(3), 'b');
|
||||
assert.equal(rangeMap.itemAt(32), 'b');
|
||||
assert.equal(rangeMap.itemAt(33), 'c');
|
||||
assert.equal(rangeMap.itemAt(57), 'c');
|
||||
assert.equal(rangeMap.itemAt(58), 'd');
|
||||
assert.equal(rangeMap.itemAt(59), 'd');
|
||||
assert.equal(rangeMap.itemAt(60), 'x');
|
||||
assert.equal(rangeMap.itemAt(63), 'x');
|
||||
assert.equal(rangeMap.itemAt(64), 'y');
|
||||
assert.equal(rangeMap.itemAt(83), 'y');
|
||||
assert.equal(rangeMap.itemAt(84), 'z');
|
||||
assert.equal(rangeMap.itemAt(91), 'z');
|
||||
assert.throws(() => rangeMap.itemAt(92));
|
||||
});
|
||||
|
||||
test('onRemoveItems at beginning', () => {
|
||||
rangeMap.onRemoveItems(new ArrayIterator(['a', 'b']));
|
||||
|
||||
assert.equal(rangeMap.itemAt(0), 'c');
|
||||
assert.equal(rangeMap.itemAt(24), 'c');
|
||||
assert.equal(rangeMap.itemAt(25), 'd');
|
||||
assert.equal(rangeMap.itemAt(26), 'd');
|
||||
assert.throws(() => rangeMap.itemAt(27));
|
||||
});
|
||||
|
||||
test('onRemoveItems in middle', () => {
|
||||
rangeMap.onRemoveItems(new ArrayIterator(['c']));
|
||||
|
||||
assert.equal(rangeMap.itemAt(0), 'a');
|
||||
assert.equal(rangeMap.itemAt(2), 'a');
|
||||
assert.equal(rangeMap.itemAt(3), 'b');
|
||||
assert.equal(rangeMap.itemAt(32), 'b');
|
||||
assert.equal(rangeMap.itemAt(33), 'd');
|
||||
assert.equal(rangeMap.itemAt(34), 'd');
|
||||
assert.throws(() => rangeMap.itemAt(35));
|
||||
});
|
||||
|
||||
test('onRemoveItems at end', () => {
|
||||
rangeMap.onRemoveItems(new ArrayIterator(['c', 'd']));
|
||||
|
||||
assert.equal(rangeMap.itemAt(0), 'a');
|
||||
assert.equal(rangeMap.itemAt(2), 'a');
|
||||
assert.equal(rangeMap.itemAt(3), 'b');
|
||||
assert.equal(rangeMap.itemAt(32), 'b');
|
||||
assert.throws(() => rangeMap.itemAt(33));
|
||||
});
|
||||
|
||||
test('onRefreshItems at beginning', () => {
|
||||
var navigator = makeNavigator('a', 1, 'b', 1);
|
||||
rangeMap.onRefreshItems(navigator);
|
||||
|
||||
assert.equal(rangeMap.itemAt(0), 'a');
|
||||
assert.equal(rangeMap.itemAt(1), 'b');
|
||||
assert.equal(rangeMap.itemAt(2), 'c');
|
||||
assert.equal(rangeMap.itemAt(26), 'c');
|
||||
assert.equal(rangeMap.itemAt(27), 'd');
|
||||
assert.equal(rangeMap.itemAt(28), 'd');
|
||||
assert.throws(() => rangeMap.itemAt(29));
|
||||
});
|
||||
|
||||
test('onRefreshItems in middle', () => {
|
||||
var navigator = makeNavigator('b', 40, 'c', 4);
|
||||
rangeMap.onRefreshItems(navigator);
|
||||
|
||||
assert.equal(rangeMap.itemAt(0), 'a');
|
||||
assert.equal(rangeMap.itemAt(2), 'a');
|
||||
assert.equal(rangeMap.itemAt(3), 'b');
|
||||
assert.equal(rangeMap.itemAt(42), 'b');
|
||||
assert.equal(rangeMap.itemAt(43), 'c');
|
||||
assert.equal(rangeMap.itemAt(46), 'c');
|
||||
assert.equal(rangeMap.itemAt(47), 'd');
|
||||
assert.equal(rangeMap.itemAt(48), 'd');
|
||||
assert.throws(() => rangeMap.itemAt(49));
|
||||
});
|
||||
|
||||
test('onRefreshItems at end', () => {
|
||||
var navigator = makeNavigator('d', 22);
|
||||
rangeMap.onRefreshItems(navigator);
|
||||
|
||||
assert.equal(rangeMap.itemAt(0), 'a');
|
||||
assert.equal(rangeMap.itemAt(2), 'a');
|
||||
assert.equal(rangeMap.itemAt(3), 'b');
|
||||
assert.equal(rangeMap.itemAt(32), 'b');
|
||||
assert.equal(rangeMap.itemAt(33), 'c');
|
||||
assert.equal(rangeMap.itemAt(57), 'c');
|
||||
assert.equal(rangeMap.itemAt(58), 'd');
|
||||
assert.equal(rangeMap.itemAt(79), 'd');
|
||||
assert.throws(() => rangeMap.itemAt(80));
|
||||
});
|
||||
|
||||
test('withItemsInRange', () => {
|
||||
var i = 0;
|
||||
var itemsInRange = ['a', 'b'];
|
||||
rangeMap.withItemsInRange(2, 27, function (item) { assert.equal(item, itemsInRange[i++]); });
|
||||
assert.equal(i, itemsInRange.length);
|
||||
|
||||
i = 0;
|
||||
itemsInRange = ['a', 'b'];
|
||||
rangeMap.withItemsInRange(0, 3, function (item) { assert.equal(item, itemsInRange[i++]); });
|
||||
assert.equal(i, itemsInRange.length);
|
||||
|
||||
i = 0;
|
||||
itemsInRange = ['a'];
|
||||
rangeMap.withItemsInRange(0, 2, function (item) { assert.equal(item, itemsInRange[i++]); });
|
||||
assert.equal(i, itemsInRange.length);
|
||||
|
||||
i = 0;
|
||||
itemsInRange = ['a'];
|
||||
rangeMap.withItemsInRange(0, 2, function (item) { assert.equal(item, itemsInRange[i++]); });
|
||||
assert.equal(i, itemsInRange.length);
|
||||
|
||||
i = 0;
|
||||
itemsInRange = ['b', 'c'];
|
||||
rangeMap.withItemsInRange(15, 39, function (item) { assert.equal(item, itemsInRange[i++]); });
|
||||
assert.equal(i, itemsInRange.length);
|
||||
|
||||
i = 0;
|
||||
itemsInRange = ['a', 'b', 'c', 'd'];
|
||||
rangeMap.withItemsInRange(1, 58, function (item) { assert.equal(item, itemsInRange[i++]); });
|
||||
assert.equal(i, itemsInRange.length);
|
||||
|
||||
i = 0;
|
||||
itemsInRange = ['c', 'd'];
|
||||
rangeMap.withItemsInRange(45, 58, function (item) { assert.equal(item, itemsInRange[i++]); });
|
||||
assert.equal(i, itemsInRange.length);
|
||||
});
|
||||
});
|
||||