Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@ -0,0 +1,42 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface ICommonContextMenuItem {
|
||||
label?: string;
|
||||
|
||||
type?: 'normal' | 'separator' | 'submenu' | 'checkbox' | 'radio';
|
||||
|
||||
accelerator?: string;
|
||||
|
||||
enabled?: boolean;
|
||||
visible?: boolean;
|
||||
checked?: boolean;
|
||||
}
|
||||
|
||||
export interface ISerializableContextMenuItem extends ICommonContextMenuItem {
|
||||
id: number;
|
||||
submenu?: ISerializableContextMenuItem[];
|
||||
}
|
||||
|
||||
export interface IContextMenuItem extends ICommonContextMenuItem {
|
||||
click?: (event: IContextMenuEvent) => void;
|
||||
submenu?: IContextMenuItem[];
|
||||
}
|
||||
|
||||
export interface IContextMenuEvent {
|
||||
shiftKey?: boolean;
|
||||
ctrlKey?: boolean;
|
||||
altKey?: boolean;
|
||||
metaKey?: boolean;
|
||||
}
|
||||
|
||||
export interface IPopupOptions {
|
||||
x?: number;
|
||||
y?: number;
|
||||
positioningItem?: number;
|
||||
}
|
||||
|
||||
export const CONTEXT_MENU_CHANNEL = 'vscode:contextmenu';
|
||||
export const CONTEXT_MENU_CLOSE_CHANNEL = 'vscode:onCloseContextMenu';
|
@ -0,0 +1,69 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Menu, MenuItem, BrowserWindow, ipcMain, IpcMainEvent } from 'electron';
|
||||
import { ISerializableContextMenuItem, CONTEXT_MENU_CLOSE_CHANNEL, CONTEXT_MENU_CHANNEL, IPopupOptions } from 'vs/base/parts/contextmenu/common/contextmenu';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
|
||||
export function registerContextMenuListener(): void {
|
||||
ipcMain.on(CONTEXT_MENU_CHANNEL, (event: IpcMainEvent, contextMenuId: number, items: ISerializableContextMenuItem[], onClickChannel: string, options?: IPopupOptions) => {
|
||||
const menu = createMenu(event, onClickChannel, items);
|
||||
|
||||
menu.popup({
|
||||
window: withNullAsUndefined(BrowserWindow.fromWebContents(event.sender)),
|
||||
x: options ? options.x : undefined,
|
||||
y: options ? options.y : undefined,
|
||||
positioningItem: options ? options.positioningItem : undefined,
|
||||
callback: () => {
|
||||
// Workaround for https://github.com/microsoft/vscode/issues/72447
|
||||
// It turns out that the menu gets GC'ed if not referenced anymore
|
||||
// As such we drag it into this scope so that it is not being GC'ed
|
||||
if (menu) {
|
||||
event.sender.send(CONTEXT_MENU_CLOSE_CHANNEL, contextMenuId);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createMenu(event: IpcMainEvent, onClickChannel: string, items: ISerializableContextMenuItem[]): Menu {
|
||||
const menu = new Menu();
|
||||
|
||||
items.forEach(item => {
|
||||
let menuitem: MenuItem;
|
||||
|
||||
// Separator
|
||||
if (item.type === 'separator') {
|
||||
menuitem = new MenuItem({
|
||||
type: item.type,
|
||||
});
|
||||
}
|
||||
|
||||
// Sub Menu
|
||||
else if (Array.isArray(item.submenu)) {
|
||||
menuitem = new MenuItem({
|
||||
submenu: createMenu(event, onClickChannel, item.submenu),
|
||||
label: item.label
|
||||
});
|
||||
}
|
||||
|
||||
// Normal Menu Item
|
||||
else {
|
||||
menuitem = new MenuItem({
|
||||
label: item.label,
|
||||
type: item.type,
|
||||
accelerator: item.accelerator,
|
||||
checked: item.checked,
|
||||
enabled: item.enabled,
|
||||
visible: item.visible,
|
||||
click: (menuItem, win, contextmenuEvent) => event.sender.send(onClickChannel, item.id, contextmenuEvent)
|
||||
});
|
||||
}
|
||||
|
||||
menu.append(menuitem);
|
||||
});
|
||||
|
||||
return menu;
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
|
||||
import { IContextMenuItem, ISerializableContextMenuItem, CONTEXT_MENU_CLOSE_CHANNEL, CONTEXT_MENU_CHANNEL, IPopupOptions, IContextMenuEvent } from 'vs/base/parts/contextmenu/common/contextmenu';
|
||||
|
||||
let contextMenuIdPool = 0;
|
||||
|
||||
export function popup(items: IContextMenuItem[], options?: IPopupOptions, onHide?: () => void): void {
|
||||
const processedItems: IContextMenuItem[] = [];
|
||||
|
||||
const contextMenuId = contextMenuIdPool++;
|
||||
const onClickChannel = `vscode:onContextMenu${contextMenuId}`;
|
||||
const onClickChannelHandler = (event: unknown, itemId: number, context: IContextMenuEvent) => {
|
||||
const item = processedItems[itemId];
|
||||
if (item.click) {
|
||||
item.click(context);
|
||||
}
|
||||
};
|
||||
|
||||
ipcRenderer.once(onClickChannel, onClickChannelHandler);
|
||||
ipcRenderer.once(CONTEXT_MENU_CLOSE_CHANNEL, (event: unknown, closedContextMenuId: number) => {
|
||||
if (closedContextMenuId !== contextMenuId) {
|
||||
return;
|
||||
}
|
||||
|
||||
ipcRenderer.removeListener(onClickChannel, onClickChannelHandler);
|
||||
|
||||
if (onHide) {
|
||||
onHide();
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.send(CONTEXT_MENU_CHANNEL, contextMenuId, items.map(item => createItem(item, processedItems)), onClickChannel, options);
|
||||
}
|
||||
|
||||
function createItem(item: IContextMenuItem, processedItems: IContextMenuItem[]): ISerializableContextMenuItem {
|
||||
const serializableItem: ISerializableContextMenuItem = {
|
||||
id: processedItems.length,
|
||||
label: item.label,
|
||||
type: item.type,
|
||||
accelerator: item.accelerator,
|
||||
checked: item.checked,
|
||||
enabled: typeof item.enabled === 'boolean' ? item.enabled : true,
|
||||
visible: typeof item.visible === 'boolean' ? item.visible : true
|
||||
};
|
||||
|
||||
processedItems.push(item);
|
||||
|
||||
// Submenu
|
||||
if (Array.isArray(item.submenu)) {
|
||||
serializableItem.submenu = item.submenu.map(submenuItem => createItem(submenuItem, processedItems));
|
||||
}
|
||||
|
||||
return serializableItem;
|
||||
}
|
29
lib/vscode/src/vs/base/parts/ipc/common/ipc.electron.ts
Normal file
29
lib/vscode/src/vs/base/parts/ipc/common/ipc.electron.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
export interface Sender {
|
||||
send(channel: string, msg: unknown): void;
|
||||
}
|
||||
|
||||
export class Protocol implements IMessagePassingProtocol {
|
||||
|
||||
constructor(private sender: Sender, readonly onMessage: Event<VSBuffer>) { }
|
||||
|
||||
send(message: VSBuffer): void {
|
||||
try {
|
||||
this.sender.send('vscode:message', message.buffer);
|
||||
} catch (e) {
|
||||
// systems are going down
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.sender.send('vscode:disconnect', null);
|
||||
}
|
||||
}
|
902
lib/vscode/src/vs/base/parts/ipc/common/ipc.net.ts
Normal file
902
lib/vscode/src/vs/base/parts/ipc/common/ipc.net.ts
Normal file
@ -0,0 +1,902 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IMessagePassingProtocol, IPCClient, IIPCLogger } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import * as process from 'vs/base/common/process';
|
||||
|
||||
export interface ISocket extends IDisposable {
|
||||
onData(listener: (e: VSBuffer) => void): IDisposable;
|
||||
onClose(listener: () => void): IDisposable;
|
||||
onEnd(listener: () => void): IDisposable;
|
||||
write(buffer: VSBuffer): void;
|
||||
end(): void;
|
||||
drain(): Promise<void>;
|
||||
}
|
||||
|
||||
let emptyBuffer: VSBuffer | null = null;
|
||||
function getEmptyBuffer(): VSBuffer {
|
||||
if (!emptyBuffer) {
|
||||
emptyBuffer = VSBuffer.alloc(0);
|
||||
}
|
||||
return emptyBuffer;
|
||||
}
|
||||
|
||||
export class ChunkStream {
|
||||
|
||||
private _chunks: VSBuffer[];
|
||||
private _totalLength: number;
|
||||
|
||||
public get byteLength() {
|
||||
return this._totalLength;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this._chunks = [];
|
||||
this._totalLength = 0;
|
||||
}
|
||||
|
||||
public acceptChunk(buff: VSBuffer) {
|
||||
this._chunks.push(buff);
|
||||
this._totalLength += buff.byteLength;
|
||||
}
|
||||
|
||||
public read(byteCount: number): VSBuffer {
|
||||
return this._read(byteCount, true);
|
||||
}
|
||||
|
||||
public peek(byteCount: number): VSBuffer {
|
||||
return this._read(byteCount, false);
|
||||
}
|
||||
|
||||
private _read(byteCount: number, advance: boolean): VSBuffer {
|
||||
|
||||
if (byteCount === 0) {
|
||||
return getEmptyBuffer();
|
||||
}
|
||||
|
||||
if (byteCount > this._totalLength) {
|
||||
throw new Error(`Cannot read so many bytes!`);
|
||||
}
|
||||
|
||||
if (this._chunks[0].byteLength === byteCount) {
|
||||
// super fast path, precisely first chunk must be returned
|
||||
const result = this._chunks[0];
|
||||
if (advance) {
|
||||
this._chunks.shift();
|
||||
this._totalLength -= byteCount;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (this._chunks[0].byteLength > byteCount) {
|
||||
// fast path, the reading is entirely within the first chunk
|
||||
const result = this._chunks[0].slice(0, byteCount);
|
||||
if (advance) {
|
||||
this._chunks[0] = this._chunks[0].slice(byteCount);
|
||||
this._totalLength -= byteCount;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
let result = VSBuffer.alloc(byteCount);
|
||||
let resultOffset = 0;
|
||||
let chunkIndex = 0;
|
||||
while (byteCount > 0) {
|
||||
const chunk = this._chunks[chunkIndex];
|
||||
if (chunk.byteLength > byteCount) {
|
||||
// this chunk will survive
|
||||
const chunkPart = chunk.slice(0, byteCount);
|
||||
result.set(chunkPart, resultOffset);
|
||||
resultOffset += byteCount;
|
||||
|
||||
if (advance) {
|
||||
this._chunks[chunkIndex] = chunk.slice(byteCount);
|
||||
this._totalLength -= byteCount;
|
||||
}
|
||||
|
||||
byteCount -= byteCount;
|
||||
} else {
|
||||
// this chunk will be entirely read
|
||||
result.set(chunk, resultOffset);
|
||||
resultOffset += chunk.byteLength;
|
||||
|
||||
if (advance) {
|
||||
this._chunks.shift();
|
||||
this._totalLength -= chunk.byteLength;
|
||||
} else {
|
||||
chunkIndex++;
|
||||
}
|
||||
|
||||
byteCount -= chunk.byteLength;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const enum ProtocolMessageType {
|
||||
None = 0,
|
||||
Regular = 1,
|
||||
Control = 2,
|
||||
Ack = 3,
|
||||
KeepAlive = 4,
|
||||
Disconnect = 5
|
||||
}
|
||||
|
||||
export const enum ProtocolConstants {
|
||||
HeaderLength = 13,
|
||||
/**
|
||||
* Send an Acknowledge message at most 2 seconds later...
|
||||
*/
|
||||
AcknowledgeTime = 2000, // 2 seconds
|
||||
/**
|
||||
* If there is a message that has been unacknowledged for 10 seconds, consider the connection closed...
|
||||
*/
|
||||
AcknowledgeTimeoutTime = 20000, // 20 seconds
|
||||
/**
|
||||
* Send at least a message every 5s for keep alive reasons.
|
||||
*/
|
||||
KeepAliveTime = 5000, // 5 seconds
|
||||
/**
|
||||
* If there is no message received for 10 seconds, consider the connection closed...
|
||||
*/
|
||||
KeepAliveTimeoutTime = 20000, // 20 seconds
|
||||
/**
|
||||
* If there is no reconnection within this time-frame, consider the connection permanently closed...
|
||||
*/
|
||||
ReconnectionGraceTime = 3 * 60 * 60 * 1000, // 3hrs
|
||||
/**
|
||||
* Maximal grace time between the first and the last reconnection...
|
||||
*/
|
||||
ReconnectionShortGraceTime = 5 * 60 * 1000, // 5min
|
||||
}
|
||||
|
||||
class ProtocolMessage {
|
||||
|
||||
public writtenTime: number;
|
||||
|
||||
constructor(
|
||||
public readonly type: ProtocolMessageType,
|
||||
public readonly id: number,
|
||||
public readonly ack: number,
|
||||
public readonly data: VSBuffer
|
||||
) {
|
||||
this.writtenTime = 0;
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this.data.byteLength;
|
||||
}
|
||||
}
|
||||
|
||||
class ProtocolReader extends Disposable {
|
||||
|
||||
private readonly _socket: ISocket;
|
||||
private _isDisposed: boolean;
|
||||
private readonly _incomingData: ChunkStream;
|
||||
public lastReadTime: number;
|
||||
|
||||
private readonly _onMessage = this._register(new Emitter<ProtocolMessage>());
|
||||
public readonly onMessage: Event<ProtocolMessage> = this._onMessage.event;
|
||||
|
||||
private readonly _state = {
|
||||
readHead: true,
|
||||
readLen: ProtocolConstants.HeaderLength,
|
||||
messageType: ProtocolMessageType.None,
|
||||
id: 0,
|
||||
ack: 0
|
||||
};
|
||||
|
||||
constructor(socket: ISocket) {
|
||||
super();
|
||||
this._socket = socket;
|
||||
this._isDisposed = false;
|
||||
this._incomingData = new ChunkStream();
|
||||
this._register(this._socket.onData(data => this.acceptChunk(data)));
|
||||
this.lastReadTime = Date.now();
|
||||
}
|
||||
|
||||
public acceptChunk(data: VSBuffer | null): void {
|
||||
if (!data || data.byteLength === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastReadTime = Date.now();
|
||||
|
||||
this._incomingData.acceptChunk(data);
|
||||
|
||||
while (this._incomingData.byteLength >= this._state.readLen) {
|
||||
|
||||
const buff = this._incomingData.read(this._state.readLen);
|
||||
|
||||
if (this._state.readHead) {
|
||||
// buff is the header
|
||||
|
||||
// save new state => next time will read the body
|
||||
this._state.readHead = false;
|
||||
this._state.readLen = buff.readUInt32BE(9);
|
||||
this._state.messageType = buff.readUInt8(0);
|
||||
this._state.id = buff.readUInt32BE(1);
|
||||
this._state.ack = buff.readUInt32BE(5);
|
||||
} else {
|
||||
// buff is the body
|
||||
const messageType = this._state.messageType;
|
||||
const id = this._state.id;
|
||||
const ack = this._state.ack;
|
||||
|
||||
// save new state => next time will read the header
|
||||
this._state.readHead = true;
|
||||
this._state.readLen = ProtocolConstants.HeaderLength;
|
||||
this._state.messageType = ProtocolMessageType.None;
|
||||
this._state.id = 0;
|
||||
this._state.ack = 0;
|
||||
|
||||
this._onMessage.fire(new ProtocolMessage(messageType, id, ack, buff));
|
||||
|
||||
if (this._isDisposed) {
|
||||
// check if an event listener lead to our disposal
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public readEntireBuffer(): VSBuffer {
|
||||
return this._incomingData.read(this._incomingData.byteLength);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._isDisposed = true;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class ProtocolWriter {
|
||||
|
||||
private _isDisposed: boolean;
|
||||
private readonly _socket: ISocket;
|
||||
private _data: VSBuffer[];
|
||||
private _totalLength: number;
|
||||
public lastWriteTime: number;
|
||||
|
||||
constructor(socket: ISocket) {
|
||||
this._isDisposed = false;
|
||||
this._socket = socket;
|
||||
this._data = [];
|
||||
this._totalLength = 0;
|
||||
this.lastWriteTime = 0;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.flush();
|
||||
this._isDisposed = true;
|
||||
}
|
||||
|
||||
public drain(): Promise<void> {
|
||||
this.flush();
|
||||
return this._socket.drain();
|
||||
}
|
||||
|
||||
public flush(): void {
|
||||
// flush
|
||||
this._writeNow();
|
||||
}
|
||||
|
||||
public write(msg: ProtocolMessage) {
|
||||
if (this._isDisposed) {
|
||||
// ignore: there could be left-over promises which complete and then
|
||||
// decide to write a response, etc...
|
||||
return;
|
||||
}
|
||||
msg.writtenTime = Date.now();
|
||||
this.lastWriteTime = Date.now();
|
||||
const header = VSBuffer.alloc(ProtocolConstants.HeaderLength);
|
||||
header.writeUInt8(msg.type, 0);
|
||||
header.writeUInt32BE(msg.id, 1);
|
||||
header.writeUInt32BE(msg.ack, 5);
|
||||
header.writeUInt32BE(msg.data.byteLength, 9);
|
||||
this._writeSoon(header, msg.data);
|
||||
}
|
||||
|
||||
private _bufferAdd(head: VSBuffer, body: VSBuffer): boolean {
|
||||
const wasEmpty = this._totalLength === 0;
|
||||
this._data.push(head, body);
|
||||
this._totalLength += head.byteLength + body.byteLength;
|
||||
return wasEmpty;
|
||||
}
|
||||
|
||||
private _bufferTake(): VSBuffer {
|
||||
const ret = VSBuffer.concat(this._data, this._totalLength);
|
||||
this._data.length = 0;
|
||||
this._totalLength = 0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
private _writeSoon(header: VSBuffer, data: VSBuffer): void {
|
||||
if (this._bufferAdd(header, data)) {
|
||||
platform.setImmediate(() => {
|
||||
this._writeNow();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _writeNow(): void {
|
||||
if (this._totalLength === 0) {
|
||||
return;
|
||||
}
|
||||
this._socket.write(this._bufferTake());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A message has the following format:
|
||||
* ```
|
||||
* /-------------------------------|------\
|
||||
* | HEADER | |
|
||||
* |-------------------------------| DATA |
|
||||
* | TYPE | ID | ACK | DATA_LENGTH | |
|
||||
* \-------------------------------|------/
|
||||
* ```
|
||||
* The header is 9 bytes and consists of:
|
||||
* - TYPE is 1 byte (ProtocolMessageType) - the message type
|
||||
* - ID is 4 bytes (u32be) - the message id (can be 0 to indicate to be ignored)
|
||||
* - ACK is 4 bytes (u32be) - the acknowledged message id (can be 0 to indicate to be ignored)
|
||||
* - DATA_LENGTH is 4 bytes (u32be) - the length in bytes of DATA
|
||||
*
|
||||
* Only Regular messages are counted, other messages are not counted, nor acknowledged.
|
||||
*/
|
||||
export class Protocol extends Disposable implements IMessagePassingProtocol {
|
||||
|
||||
private _socket: ISocket;
|
||||
private _socketWriter: ProtocolWriter;
|
||||
private _socketReader: ProtocolReader;
|
||||
|
||||
private readonly _onMessage = new Emitter<VSBuffer>();
|
||||
readonly onMessage: Event<VSBuffer> = this._onMessage.event;
|
||||
|
||||
private readonly _onClose = new Emitter<void>();
|
||||
readonly onClose: Event<void> = this._onClose.event;
|
||||
|
||||
constructor(socket: ISocket) {
|
||||
super();
|
||||
this._socket = socket;
|
||||
this._socketWriter = this._register(new ProtocolWriter(this._socket));
|
||||
this._socketReader = this._register(new ProtocolReader(this._socket));
|
||||
|
||||
this._register(this._socketReader.onMessage((msg) => {
|
||||
if (msg.type === ProtocolMessageType.Regular) {
|
||||
this._onMessage.fire(msg.data);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this._socket.onClose(() => this._onClose.fire()));
|
||||
}
|
||||
|
||||
drain(): Promise<void> {
|
||||
return this._socketWriter.drain();
|
||||
}
|
||||
|
||||
getSocket(): ISocket {
|
||||
return this._socket;
|
||||
}
|
||||
|
||||
sendDisconnect(): void {
|
||||
// Nothing to do...
|
||||
}
|
||||
|
||||
send(buffer: VSBuffer): void {
|
||||
this._socketWriter.write(new ProtocolMessage(ProtocolMessageType.Regular, 0, 0, buffer));
|
||||
}
|
||||
}
|
||||
|
||||
export class Client<TContext = string> extends IPCClient<TContext> {
|
||||
|
||||
static fromSocket<TContext = string>(socket: ISocket, id: TContext): Client<TContext> {
|
||||
return new Client(new Protocol(socket), id);
|
||||
}
|
||||
|
||||
get onClose(): Event<void> { return this.protocol.onClose; }
|
||||
|
||||
constructor(private protocol: Protocol | PersistentProtocol, id: TContext, ipcLogger: IIPCLogger | null = null) {
|
||||
super(protocol, id, ipcLogger);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
const socket = this.protocol.getSocket();
|
||||
this.protocol.sendDisconnect();
|
||||
this.protocol.dispose();
|
||||
socket.end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will ensure no messages are lost if there are no event listeners.
|
||||
*/
|
||||
export class BufferedEmitter<T> {
|
||||
private _emitter: Emitter<T>;
|
||||
public readonly event: Event<T>;
|
||||
|
||||
private _hasListeners = false;
|
||||
private _isDeliveringMessages = false;
|
||||
private _bufferedMessages: T[] = [];
|
||||
|
||||
constructor() {
|
||||
this._emitter = new Emitter<T>({
|
||||
onFirstListenerAdd: () => {
|
||||
this._hasListeners = true;
|
||||
// it is important to deliver these messages after this call, but before
|
||||
// other messages have a chance to be received (to guarantee in order delivery)
|
||||
// that's why we're using here nextTick and not other types of timeouts
|
||||
process.nextTick(() => this._deliverMessages());
|
||||
},
|
||||
onLastListenerRemove: () => {
|
||||
this._hasListeners = false;
|
||||
}
|
||||
});
|
||||
|
||||
this.event = this._emitter.event;
|
||||
}
|
||||
|
||||
private _deliverMessages(): void {
|
||||
if (this._isDeliveringMessages) {
|
||||
return;
|
||||
}
|
||||
this._isDeliveringMessages = true;
|
||||
while (this._hasListeners && this._bufferedMessages.length > 0) {
|
||||
this._emitter.fire(this._bufferedMessages.shift()!);
|
||||
}
|
||||
this._isDeliveringMessages = false;
|
||||
}
|
||||
|
||||
public fire(event: T): void {
|
||||
if (this._hasListeners) {
|
||||
if (this._bufferedMessages.length > 0) {
|
||||
this._bufferedMessages.push(event);
|
||||
} else {
|
||||
this._emitter.fire(event);
|
||||
}
|
||||
} else {
|
||||
this._bufferedMessages.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
public flushBuffer(): void {
|
||||
this._bufferedMessages = [];
|
||||
}
|
||||
}
|
||||
|
||||
class QueueElement<T> {
|
||||
public readonly data: T;
|
||||
public next: QueueElement<T> | null;
|
||||
|
||||
constructor(data: T) {
|
||||
this.data = data;
|
||||
this.next = null;
|
||||
}
|
||||
}
|
||||
|
||||
class Queue<T> {
|
||||
|
||||
private _first: QueueElement<T> | null;
|
||||
private _last: QueueElement<T> | null;
|
||||
|
||||
constructor() {
|
||||
this._first = null;
|
||||
this._last = null;
|
||||
}
|
||||
|
||||
public peek(): T | null {
|
||||
if (!this._first) {
|
||||
return null;
|
||||
}
|
||||
return this._first.data;
|
||||
}
|
||||
|
||||
public toArray(): T[] {
|
||||
let result: T[] = [], resultLen = 0;
|
||||
let it = this._first;
|
||||
while (it) {
|
||||
result[resultLen++] = it.data;
|
||||
it = it.next;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public pop(): void {
|
||||
if (!this._first) {
|
||||
return;
|
||||
}
|
||||
if (this._first === this._last) {
|
||||
this._first = null;
|
||||
this._last = null;
|
||||
return;
|
||||
}
|
||||
this._first = this._first.next;
|
||||
}
|
||||
|
||||
public push(item: T): void {
|
||||
const element = new QueueElement(item);
|
||||
if (!this._first) {
|
||||
this._first = element;
|
||||
this._last = element;
|
||||
return;
|
||||
}
|
||||
this._last!.next = element;
|
||||
this._last = element;
|
||||
}
|
||||
}
|
||||
|
||||
class LoadEstimator {
|
||||
|
||||
private static _HISTORY_LENGTH = 10;
|
||||
private static _INSTANCE: LoadEstimator | null = null;
|
||||
public static getInstance(): LoadEstimator {
|
||||
if (!LoadEstimator._INSTANCE) {
|
||||
LoadEstimator._INSTANCE = new LoadEstimator();
|
||||
}
|
||||
return LoadEstimator._INSTANCE;
|
||||
}
|
||||
|
||||
private lastRuns: number[];
|
||||
|
||||
constructor() {
|
||||
this.lastRuns = [];
|
||||
const now = Date.now();
|
||||
for (let i = 0; i < LoadEstimator._HISTORY_LENGTH; i++) {
|
||||
this.lastRuns[i] = now - 1000 * i;
|
||||
}
|
||||
setInterval(() => {
|
||||
for (let i = LoadEstimator._HISTORY_LENGTH; i >= 1; i--) {
|
||||
this.lastRuns[i] = this.lastRuns[i - 1];
|
||||
}
|
||||
this.lastRuns[0] = Date.now();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns an estimative number, from 0 (low load) to 1 (high load)
|
||||
*/
|
||||
public load(): number {
|
||||
const now = Date.now();
|
||||
const historyLimit = (1 + LoadEstimator._HISTORY_LENGTH) * 1000;
|
||||
let score = 0;
|
||||
for (let i = 0; i < LoadEstimator._HISTORY_LENGTH; i++) {
|
||||
if (now - this.lastRuns[i] <= historyLimit) {
|
||||
score++;
|
||||
}
|
||||
}
|
||||
return 1 - score / LoadEstimator._HISTORY_LENGTH;
|
||||
}
|
||||
|
||||
public hasHighLoad(): boolean {
|
||||
return this.load() >= 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as Protocol, but will actually track messages and acks.
|
||||
* Moreover, it will ensure no messages are lost if there are no event listeners.
|
||||
*/
|
||||
export class PersistentProtocol implements IMessagePassingProtocol {
|
||||
|
||||
private _isReconnecting: boolean;
|
||||
|
||||
private _outgoingUnackMsg: Queue<ProtocolMessage>;
|
||||
private _outgoingMsgId: number;
|
||||
private _outgoingAckId: number;
|
||||
private _outgoingAckTimeout: any | null;
|
||||
|
||||
private _incomingMsgId: number;
|
||||
private _incomingAckId: number;
|
||||
private _incomingMsgLastTime: number;
|
||||
private _incomingAckTimeout: any | null;
|
||||
|
||||
private _outgoingKeepAliveTimeout: any | null;
|
||||
private _incomingKeepAliveTimeout: any | null;
|
||||
|
||||
private _socket: ISocket;
|
||||
private _socketWriter: ProtocolWriter;
|
||||
private _socketReader: ProtocolReader;
|
||||
private _socketDisposables: IDisposable[];
|
||||
|
||||
private readonly _loadEstimator = LoadEstimator.getInstance();
|
||||
|
||||
private readonly _onControlMessage = new BufferedEmitter<VSBuffer>();
|
||||
readonly onControlMessage: Event<VSBuffer> = this._onControlMessage.event;
|
||||
|
||||
private readonly _onMessage = new BufferedEmitter<VSBuffer>();
|
||||
readonly onMessage: Event<VSBuffer> = this._onMessage.event;
|
||||
|
||||
private readonly _onClose = new BufferedEmitter<void>();
|
||||
readonly onClose: Event<void> = this._onClose.event;
|
||||
|
||||
private readonly _onSocketClose = new BufferedEmitter<void>();
|
||||
readonly onSocketClose: Event<void> = this._onSocketClose.event;
|
||||
|
||||
private readonly _onSocketTimeout = new BufferedEmitter<void>();
|
||||
readonly onSocketTimeout: Event<void> = this._onSocketTimeout.event;
|
||||
|
||||
public get unacknowledgedCount(): number {
|
||||
return this._outgoingMsgId - this._outgoingAckId;
|
||||
}
|
||||
|
||||
constructor(socket: ISocket, initialChunk: VSBuffer | null = null) {
|
||||
this._isReconnecting = false;
|
||||
this._outgoingUnackMsg = new Queue<ProtocolMessage>();
|
||||
this._outgoingMsgId = 0;
|
||||
this._outgoingAckId = 0;
|
||||
this._outgoingAckTimeout = null;
|
||||
|
||||
this._incomingMsgId = 0;
|
||||
this._incomingAckId = 0;
|
||||
this._incomingMsgLastTime = 0;
|
||||
this._incomingAckTimeout = null;
|
||||
|
||||
this._outgoingKeepAliveTimeout = null;
|
||||
this._incomingKeepAliveTimeout = null;
|
||||
|
||||
this._socketDisposables = [];
|
||||
this._socket = socket;
|
||||
this._socketWriter = new ProtocolWriter(this._socket);
|
||||
this._socketDisposables.push(this._socketWriter);
|
||||
this._socketReader = new ProtocolReader(this._socket);
|
||||
this._socketDisposables.push(this._socketReader);
|
||||
this._socketDisposables.push(this._socketReader.onMessage(msg => this._receiveMessage(msg)));
|
||||
this._socketDisposables.push(this._socket.onClose(() => this._onSocketClose.fire()));
|
||||
if (initialChunk) {
|
||||
this._socketReader.acceptChunk(initialChunk);
|
||||
}
|
||||
|
||||
this._sendKeepAliveCheck();
|
||||
this._recvKeepAliveCheck();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this._outgoingAckTimeout) {
|
||||
clearTimeout(this._outgoingAckTimeout);
|
||||
this._outgoingAckTimeout = null;
|
||||
}
|
||||
if (this._incomingAckTimeout) {
|
||||
clearTimeout(this._incomingAckTimeout);
|
||||
this._incomingAckTimeout = null;
|
||||
}
|
||||
if (this._outgoingKeepAliveTimeout) {
|
||||
clearTimeout(this._outgoingKeepAliveTimeout);
|
||||
this._outgoingKeepAliveTimeout = null;
|
||||
}
|
||||
if (this._incomingKeepAliveTimeout) {
|
||||
clearTimeout(this._incomingKeepAliveTimeout);
|
||||
this._incomingKeepAliveTimeout = null;
|
||||
}
|
||||
this._socketDisposables = dispose(this._socketDisposables);
|
||||
}
|
||||
|
||||
drain(): Promise<void> {
|
||||
return this._socketWriter.drain();
|
||||
}
|
||||
|
||||
sendDisconnect(): void {
|
||||
const msg = new ProtocolMessage(ProtocolMessageType.Disconnect, 0, 0, getEmptyBuffer());
|
||||
this._socketWriter.write(msg);
|
||||
this._socketWriter.flush();
|
||||
}
|
||||
|
||||
private _sendKeepAliveCheck(): void {
|
||||
if (this._outgoingKeepAliveTimeout) {
|
||||
// there will be a check in the near future
|
||||
return;
|
||||
}
|
||||
|
||||
const timeSinceLastOutgoingMsg = Date.now() - this._socketWriter.lastWriteTime;
|
||||
if (timeSinceLastOutgoingMsg >= ProtocolConstants.KeepAliveTime) {
|
||||
// sufficient time has passed since last message was written,
|
||||
// and no message from our side needed to be sent in the meantime,
|
||||
// so we will send a message containing only a keep alive.
|
||||
const msg = new ProtocolMessage(ProtocolMessageType.KeepAlive, 0, 0, getEmptyBuffer());
|
||||
this._socketWriter.write(msg);
|
||||
this._sendKeepAliveCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
this._outgoingKeepAliveTimeout = setTimeout(() => {
|
||||
this._outgoingKeepAliveTimeout = null;
|
||||
this._sendKeepAliveCheck();
|
||||
}, ProtocolConstants.KeepAliveTime - timeSinceLastOutgoingMsg + 5);
|
||||
}
|
||||
|
||||
private _recvKeepAliveCheck(): void {
|
||||
if (this._incomingKeepAliveTimeout) {
|
||||
// there will be a check in the near future
|
||||
return;
|
||||
}
|
||||
|
||||
const timeSinceLastIncomingMsg = Date.now() - this._socketReader.lastReadTime;
|
||||
if (timeSinceLastIncomingMsg >= ProtocolConstants.KeepAliveTimeoutTime) {
|
||||
// It's been a long time since we received a server message
|
||||
// But this might be caused by the event loop being busy and failing to read messages
|
||||
if (!this._loadEstimator.hasHighLoad()) {
|
||||
// Trash the socket
|
||||
this._onSocketTimeout.fire(undefined);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._incomingKeepAliveTimeout = setTimeout(() => {
|
||||
this._incomingKeepAliveTimeout = null;
|
||||
this._recvKeepAliveCheck();
|
||||
}, Math.max(ProtocolConstants.KeepAliveTimeoutTime - timeSinceLastIncomingMsg, 0) + 5);
|
||||
}
|
||||
|
||||
public getSocket(): ISocket {
|
||||
return this._socket;
|
||||
}
|
||||
|
||||
public beginAcceptReconnection(socket: ISocket, initialDataChunk: VSBuffer | null): void {
|
||||
this._isReconnecting = true;
|
||||
|
||||
this._socketDisposables = dispose(this._socketDisposables);
|
||||
this._onControlMessage.flushBuffer();
|
||||
this._onSocketClose.flushBuffer();
|
||||
this._onSocketTimeout.flushBuffer();
|
||||
this._socket.dispose();
|
||||
|
||||
this._socket = socket;
|
||||
this._socketWriter = new ProtocolWriter(this._socket);
|
||||
this._socketDisposables.push(this._socketWriter);
|
||||
this._socketReader = new ProtocolReader(this._socket);
|
||||
this._socketDisposables.push(this._socketReader);
|
||||
this._socketDisposables.push(this._socketReader.onMessage(msg => this._receiveMessage(msg)));
|
||||
this._socketDisposables.push(this._socket.onClose(() => this._onSocketClose.fire()));
|
||||
this._socketReader.acceptChunk(initialDataChunk);
|
||||
}
|
||||
|
||||
public endAcceptReconnection(): void {
|
||||
this._isReconnecting = false;
|
||||
|
||||
// Send again all unacknowledged messages
|
||||
const toSend = this._outgoingUnackMsg.toArray();
|
||||
for (let i = 0, len = toSend.length; i < len; i++) {
|
||||
this._socketWriter.write(toSend[i]);
|
||||
}
|
||||
this._recvAckCheck();
|
||||
|
||||
this._sendKeepAliveCheck();
|
||||
this._recvKeepAliveCheck();
|
||||
}
|
||||
|
||||
public acceptDisconnect(): void {
|
||||
this._onClose.fire();
|
||||
}
|
||||
|
||||
private _receiveMessage(msg: ProtocolMessage): void {
|
||||
if (msg.ack > this._outgoingAckId) {
|
||||
this._outgoingAckId = msg.ack;
|
||||
do {
|
||||
const first = this._outgoingUnackMsg.peek();
|
||||
if (first && first.id <= msg.ack) {
|
||||
// this message has been confirmed, remove it
|
||||
this._outgoingUnackMsg.pop();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
|
||||
if (msg.type === ProtocolMessageType.Regular) {
|
||||
if (msg.id > this._incomingMsgId) {
|
||||
if (msg.id !== this._incomingMsgId + 1) {
|
||||
console.error(`PROTOCOL CORRUPTION, LAST SAW MSG ${this._incomingMsgId} AND HAVE NOW RECEIVED MSG ${msg.id}`);
|
||||
}
|
||||
this._incomingMsgId = msg.id;
|
||||
this._incomingMsgLastTime = Date.now();
|
||||
this._sendAckCheck();
|
||||
this._onMessage.fire(msg.data);
|
||||
}
|
||||
} else if (msg.type === ProtocolMessageType.Control) {
|
||||
this._onControlMessage.fire(msg.data);
|
||||
} else if (msg.type === ProtocolMessageType.Disconnect) {
|
||||
this._onClose.fire();
|
||||
}
|
||||
}
|
||||
|
||||
readEntireBuffer(): VSBuffer {
|
||||
return this._socketReader.readEntireBuffer();
|
||||
}
|
||||
|
||||
flush(): void {
|
||||
this._socketWriter.flush();
|
||||
}
|
||||
|
||||
send(buffer: VSBuffer): void {
|
||||
const myId = ++this._outgoingMsgId;
|
||||
this._incomingAckId = this._incomingMsgId;
|
||||
const msg = new ProtocolMessage(ProtocolMessageType.Regular, myId, this._incomingAckId, buffer);
|
||||
this._outgoingUnackMsg.push(msg);
|
||||
if (!this._isReconnecting) {
|
||||
this._socketWriter.write(msg);
|
||||
this._recvAckCheck();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message which will not be part of the regular acknowledge flow.
|
||||
* Use this for early control messages which are repeated in case of reconnection.
|
||||
*/
|
||||
sendControl(buffer: VSBuffer): void {
|
||||
const msg = new ProtocolMessage(ProtocolMessageType.Control, 0, 0, buffer);
|
||||
this._socketWriter.write(msg);
|
||||
}
|
||||
|
||||
private _sendAckCheck(): void {
|
||||
if (this._incomingMsgId <= this._incomingAckId) {
|
||||
// nothink to acknowledge
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._incomingAckTimeout) {
|
||||
// there will be a check in the near future
|
||||
return;
|
||||
}
|
||||
|
||||
const timeSinceLastIncomingMsg = Date.now() - this._incomingMsgLastTime;
|
||||
if (timeSinceLastIncomingMsg >= ProtocolConstants.AcknowledgeTime) {
|
||||
// sufficient time has passed since this message has been received,
|
||||
// and no message from our side needed to be sent in the meantime,
|
||||
// so we will send a message containing only an ack.
|
||||
this._sendAck();
|
||||
return;
|
||||
}
|
||||
|
||||
this._incomingAckTimeout = setTimeout(() => {
|
||||
this._incomingAckTimeout = null;
|
||||
this._sendAckCheck();
|
||||
}, ProtocolConstants.AcknowledgeTime - timeSinceLastIncomingMsg + 5);
|
||||
}
|
||||
|
||||
private _recvAckCheck(): void {
|
||||
if (this._outgoingMsgId <= this._outgoingAckId) {
|
||||
// everything has been acknowledged
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._outgoingAckTimeout) {
|
||||
// there will be a check in the near future
|
||||
return;
|
||||
}
|
||||
|
||||
const oldestUnacknowledgedMsg = this._outgoingUnackMsg.peek()!;
|
||||
const timeSinceOldestUnacknowledgedMsg = Date.now() - oldestUnacknowledgedMsg.writtenTime;
|
||||
if (timeSinceOldestUnacknowledgedMsg >= ProtocolConstants.AcknowledgeTimeoutTime) {
|
||||
// It's been a long time since our sent message was acknowledged
|
||||
// But this might be caused by the event loop being busy and failing to read messages
|
||||
if (!this._loadEstimator.hasHighLoad()) {
|
||||
// Trash the socket
|
||||
this._onSocketTimeout.fire(undefined);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._outgoingAckTimeout = setTimeout(() => {
|
||||
this._outgoingAckTimeout = null;
|
||||
this._recvAckCheck();
|
||||
}, Math.max(ProtocolConstants.AcknowledgeTimeoutTime - timeSinceOldestUnacknowledgedMsg, 0) + 5);
|
||||
}
|
||||
|
||||
private _sendAck(): void {
|
||||
if (this._incomingMsgId <= this._incomingAckId) {
|
||||
// nothink to acknowledge
|
||||
return;
|
||||
}
|
||||
|
||||
this._incomingAckId = this._incomingMsgId;
|
||||
const msg = new ProtocolMessage(ProtocolMessageType.Ack, 0, this._incomingAckId, getEmptyBuffer());
|
||||
this._socketWriter.write(msg);
|
||||
}
|
||||
}
|
1216
lib/vscode/src/vs/base/parts/ipc/common/ipc.ts
Normal file
1216
lib/vscode/src/vs/base/parts/ipc/common/ipc.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,53 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IPCServer, ClientConnectionEvent } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Protocol } from 'vs/base/parts/ipc/common/ipc.electron';
|
||||
import { ipcMain, WebContents } from 'electron';
|
||||
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
interface IIPCEvent {
|
||||
event: { sender: WebContents; };
|
||||
message: Buffer | null;
|
||||
}
|
||||
|
||||
function createScopedOnMessageEvent(senderId: number, eventName: string): Event<VSBuffer | null> {
|
||||
const onMessage = Event.fromNodeEventEmitter<IIPCEvent>(ipcMain, eventName, (event, message) => ({ event, message }));
|
||||
const onMessageFromSender = Event.filter(onMessage, ({ event }) => event.sender.id === senderId);
|
||||
return Event.map(onMessageFromSender, ({ message }) => message ? VSBuffer.wrap(message) : message);
|
||||
}
|
||||
|
||||
export class Server extends IPCServer {
|
||||
|
||||
private static readonly Clients = new Map<number, IDisposable>();
|
||||
|
||||
private static getOnDidClientConnect(): Event<ClientConnectionEvent> {
|
||||
const onHello = Event.fromNodeEventEmitter<WebContents>(ipcMain, 'vscode:hello', ({ sender }) => sender);
|
||||
|
||||
return Event.map(onHello, webContents => {
|
||||
const id = webContents.id;
|
||||
const client = Server.Clients.get(id);
|
||||
|
||||
if (client) {
|
||||
client.dispose();
|
||||
}
|
||||
|
||||
const onDidClientReconnect = new Emitter<void>();
|
||||
Server.Clients.set(id, toDisposable(() => onDidClientReconnect.fire()));
|
||||
|
||||
const onMessage = createScopedOnMessageEvent(id, 'vscode:message') as Event<VSBuffer>;
|
||||
const onDidClientDisconnect = Event.any(Event.signal(createScopedOnMessageEvent(id, 'vscode:disconnect')), onDidClientReconnect.event);
|
||||
const protocol = new Protocol(webContents, onMessage);
|
||||
|
||||
return { protocol, onDidClientDisconnect };
|
||||
});
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(Server.getOnDidClientConnect());
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IPCClient } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Protocol } from 'vs/base/parts/ipc/common/ipc.electron';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
|
||||
|
||||
export class Client extends IPCClient implements IDisposable {
|
||||
|
||||
private protocol: Protocol;
|
||||
|
||||
private static createProtocol(): Protocol {
|
||||
const onMessage = Event.fromNodeEventEmitter<VSBuffer>(ipcRenderer, 'vscode:message', (_, message) => VSBuffer.wrap(message));
|
||||
ipcRenderer.send('vscode:hello');
|
||||
return new Protocol(ipcRenderer, onMessage);
|
||||
}
|
||||
|
||||
constructor(id: string) {
|
||||
const protocol = Client.createProtocol();
|
||||
super(protocol, id);
|
||||
this.protocol = protocol;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.protocol.dispose();
|
||||
}
|
||||
}
|
288
lib/vscode/src/vs/base/parts/ipc/node/ipc.cp.ts
Normal file
288
lib/vscode/src/vs/base/parts/ipc/node/ipc.cp.ts
Normal file
@ -0,0 +1,288 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChildProcess, fork, ForkOptions } from 'child_process';
|
||||
import { IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { Delayer, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { deepClone } from 'vs/base/common/objects';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { createQueuedSender } from 'vs/base/node/processes';
|
||||
import { IChannel, ChannelServer as IPCServer, ChannelClient as IPCClient, IChannelClient } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { isRemoteConsoleLog, log } from 'vs/base/common/console';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
|
||||
/**
|
||||
* This implementation doesn't perform well since it uses base64 encoding for buffers.
|
||||
* We should move all implementations to use named ipc.net, so we stop depending on cp.fork.
|
||||
*/
|
||||
|
||||
export class Server<TContext extends string> extends IPCServer<TContext> {
|
||||
constructor(ctx: TContext) {
|
||||
super({
|
||||
send: r => {
|
||||
try {
|
||||
if (process.send) {
|
||||
process.send((<Buffer>r.buffer).toString('base64'));
|
||||
}
|
||||
} catch (e) { /* not much to do */ }
|
||||
},
|
||||
onMessage: Event.fromNodeEventEmitter(process, 'message', msg => VSBuffer.wrap(Buffer.from(msg, 'base64')))
|
||||
}, ctx);
|
||||
|
||||
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> | undefined;
|
||||
private activeRequests = new Set<IDisposable>();
|
||||
private child: ChildProcess | null;
|
||||
private _client: IPCClient | null;
|
||||
private channels = new Map<string, IChannel>();
|
||||
|
||||
private readonly _onDidProcessExit = new Emitter<{ code: number, signal: string }>();
|
||||
readonly onDidProcessExit = this._onDidProcessExit.event;
|
||||
|
||||
constructor(private modulePath: string, private options: IIPCOptions) {
|
||||
const timeout = options && options.timeout ? options.timeout : 60000;
|
||||
this.disposeDelayer = new Delayer<void>(timeout);
|
||||
this.child = null;
|
||||
this._client = null;
|
||||
}
|
||||
|
||||
getChannel<T extends IChannel>(channelName: string): T {
|
||||
const that = this;
|
||||
|
||||
return {
|
||||
call<T>(command: string, arg?: any, cancellationToken?: CancellationToken): Promise<T> {
|
||||
return that.requestPromise<T>(channelName, command, arg, cancellationToken);
|
||||
},
|
||||
listen(event: string, arg?: any) {
|
||||
return that.requestEvent(channelName, event, arg);
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
protected requestPromise<T>(channelName: string, name: string, arg?: any, cancellationToken = CancellationToken.None): Promise<T> {
|
||||
if (!this.disposeDelayer) {
|
||||
return Promise.reject(new Error('disposed'));
|
||||
}
|
||||
|
||||
if (cancellationToken.isCancellationRequested) {
|
||||
return Promise.reject(errors.canceled());
|
||||
}
|
||||
|
||||
this.disposeDelayer.cancel();
|
||||
|
||||
const channel = this.getCachedChannel(channelName);
|
||||
const result = createCancelablePromise(token => channel.call<T>(name, arg, token));
|
||||
const cancellationTokenListener = cancellationToken.onCancellationRequested(() => result.cancel());
|
||||
|
||||
const disposable = toDisposable(() => result.cancel());
|
||||
this.activeRequests.add(disposable);
|
||||
|
||||
result.finally(() => {
|
||||
cancellationTokenListener.dispose();
|
||||
this.activeRequests.delete(disposable);
|
||||
|
||||
if (this.activeRequests.size === 0 && this.disposeDelayer) {
|
||||
this.disposeDelayer.trigger(() => this.disposeClient());
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected requestEvent<T>(channelName: string, name: string, arg?: any): Event<T> {
|
||||
if (!this.disposeDelayer) {
|
||||
return Event.None;
|
||||
}
|
||||
|
||||
this.disposeDelayer.cancel();
|
||||
|
||||
let listener: IDisposable;
|
||||
const emitter = new Emitter<any>({
|
||||
onFirstListenerAdd: () => {
|
||||
const channel = this.getCachedChannel(channelName);
|
||||
const event: Event<T> = channel.listen(name, arg);
|
||||
|
||||
listener = event(emitter.fire, emitter);
|
||||
this.activeRequests.add(listener);
|
||||
},
|
||||
onLastListenerRemove: () => {
|
||||
this.activeRequests.delete(listener);
|
||||
listener.dispose();
|
||||
|
||||
if (this.activeRequests.size === 0 && this.disposeDelayer) {
|
||||
this.disposeDelayer.trigger(() => this.disposeClient());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return emitter.event;
|
||||
}
|
||||
|
||||
private get client(): IPCClient {
|
||||
if (!this._client) {
|
||||
const args = this.options && this.options.args ? this.options.args : [];
|
||||
const forkOpts: ForkOptions = Object.create(null);
|
||||
|
||||
forkOpts.env = { ...deepClone(process.env), 'VSCODE_PARENT_PID': String(process.pid) };
|
||||
|
||||
if (this.options && this.options.env) {
|
||||
forkOpts.env = { ...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];
|
||||
}
|
||||
|
||||
if (isMacintosh && forkOpts.env) {
|
||||
// Unset `DYLD_LIBRARY_PATH`, as it leads to process crashes
|
||||
// See https://github.com/microsoft/vscode/issues/105848
|
||||
delete forkOpts.env['DYLD_LIBRARY_PATH'];
|
||||
}
|
||||
|
||||
this.child = fork(this.modulePath, args, forkOpts);
|
||||
|
||||
const onMessageEmitter = new Emitter<VSBuffer>();
|
||||
const onRawMessage = Event.fromNodeEventEmitter(this.child, 'message', msg => msg);
|
||||
|
||||
onRawMessage(msg => {
|
||||
|
||||
// Handle remote console logs specially
|
||||
if (isRemoteConsoleLog(msg)) {
|
||||
log(msg, `IPC Library: ${this.options.serverName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Anything else goes to the outside
|
||||
onMessageEmitter.fire(VSBuffer.wrap(Buffer.from(msg, 'base64')));
|
||||
});
|
||||
|
||||
const sender = this.options.useQueue ? createQueuedSender(this.child) : this.child;
|
||||
const send = (r: VSBuffer) => this.child && this.child.connected && sender.send((<Buffer>r.buffer).toString('base64'));
|
||||
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' as 'loaded', onExit); // https://github.com/electron/electron/issues/21475
|
||||
|
||||
this.activeRequests.forEach(r => dispose(r));
|
||||
this.activeRequests.clear();
|
||||
|
||||
if (code !== 0 && signal !== 'SIGTERM') {
|
||||
console.warn('IPC "' + this.options.serverName + '" crashed with exit code ' + code + ' and signal ' + signal);
|
||||
}
|
||||
|
||||
if (this.disposeDelayer) {
|
||||
this.disposeDelayer.cancel();
|
||||
}
|
||||
this.disposeClient();
|
||||
this._onDidProcessExit.fire({ code, signal });
|
||||
});
|
||||
}
|
||||
|
||||
return this._client;
|
||||
}
|
||||
|
||||
private getCachedChannel(name: string): IChannel {
|
||||
let channel = this.channels.get(name);
|
||||
|
||||
if (!channel) {
|
||||
channel = this.client.getChannel(name);
|
||||
this.channels.set(name, channel);
|
||||
}
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
private disposeClient() {
|
||||
if (this._client) {
|
||||
if (this.child) {
|
||||
this.child.kill();
|
||||
this.child = null;
|
||||
}
|
||||
this._client = null;
|
||||
this.channels.clear();
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._onDidProcessExit.dispose();
|
||||
if (this.disposeDelayer) {
|
||||
this.disposeDelayer.cancel();
|
||||
this.disposeDelayer = undefined;
|
||||
}
|
||||
this.disposeClient();
|
||||
this.activeRequests.clear();
|
||||
}
|
||||
}
|
418
lib/vscode/src/vs/base/parts/ipc/node/ipc.net.ts
Normal file
418
lib/vscode/src/vs/base/parts/ipc/node/ipc.net.ts
Normal file
@ -0,0 +1,418 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createHash } from 'crypto';
|
||||
import { Socket, Server as NetServer, createConnection, createServer } from 'net';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { ClientConnectionEvent, IPCServer } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { tmpdir } from 'os';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { ISocket, Protocol, Client, ChunkStream } from 'vs/base/parts/ipc/common/ipc.net';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { Platform, platform } from 'vs/base/common/platform';
|
||||
|
||||
export class NodeSocket implements ISocket {
|
||||
public readonly socket: Socket;
|
||||
|
||||
constructor(socket: Socket) {
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.socket.destroy();
|
||||
}
|
||||
|
||||
public onData(_listener: (e: VSBuffer) => void): IDisposable {
|
||||
const listener = (buff: Buffer) => _listener(VSBuffer.wrap(buff));
|
||||
this.socket.on('data', listener);
|
||||
return {
|
||||
dispose: () => this.socket.off('data', listener)
|
||||
};
|
||||
}
|
||||
|
||||
public onClose(listener: () => void): IDisposable {
|
||||
this.socket.on('close', listener);
|
||||
return {
|
||||
dispose: () => this.socket.off('close', listener)
|
||||
};
|
||||
}
|
||||
|
||||
public onEnd(listener: () => void): IDisposable {
|
||||
this.socket.on('end', listener);
|
||||
return {
|
||||
dispose: () => this.socket.off('end', listener)
|
||||
};
|
||||
}
|
||||
|
||||
public write(buffer: VSBuffer): void {
|
||||
// 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 been allowed to drain.
|
||||
try {
|
||||
this.socket.write(<Buffer>buffer.buffer);
|
||||
} catch (err) {
|
||||
if (err.code === 'EPIPE') {
|
||||
// An EPIPE exception at the wrong time can lead to a renderer process crash
|
||||
// so ignore the error since the socket will fire the close event soon anyways:
|
||||
// > https://nodejs.org/api/errors.html#errors_common_system_errors
|
||||
// > EPIPE (Broken pipe): A write on a pipe, socket, or FIFO for which there is no
|
||||
// > process to read the data. Commonly encountered at the net and http layers,
|
||||
// > indicative that the remote side of the stream being written to has been closed.
|
||||
return;
|
||||
}
|
||||
onUnexpectedError(err);
|
||||
}
|
||||
}
|
||||
|
||||
public end(): void {
|
||||
this.socket.end();
|
||||
}
|
||||
|
||||
public drain(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (this.socket.bufferSize === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const finished = () => {
|
||||
this.socket.off('close', finished);
|
||||
this.socket.off('end', finished);
|
||||
this.socket.off('error', finished);
|
||||
this.socket.off('timeout', finished);
|
||||
this.socket.off('drain', finished);
|
||||
resolve();
|
||||
};
|
||||
this.socket.on('close', finished);
|
||||
this.socket.on('end', finished);
|
||||
this.socket.on('error', finished);
|
||||
this.socket.on('timeout', finished);
|
||||
this.socket.on('drain', finished);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const enum Constants {
|
||||
MinHeaderByteSize = 2
|
||||
}
|
||||
|
||||
const enum ReadState {
|
||||
PeekHeader = 1,
|
||||
ReadHeader = 2,
|
||||
ReadBody = 3,
|
||||
Fin = 4
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://tools.ietf.org/html/rfc6455#section-5.2
|
||||
*/
|
||||
export class WebSocketNodeSocket extends Disposable implements ISocket {
|
||||
|
||||
public readonly socket: NodeSocket;
|
||||
private readonly _incomingData: ChunkStream;
|
||||
private readonly _onData = this._register(new Emitter<VSBuffer>());
|
||||
|
||||
private readonly _state = {
|
||||
state: ReadState.PeekHeader,
|
||||
readLen: Constants.MinHeaderByteSize,
|
||||
mask: 0
|
||||
};
|
||||
|
||||
constructor(socket: NodeSocket) {
|
||||
super();
|
||||
this.socket = socket;
|
||||
this._incomingData = new ChunkStream();
|
||||
this._register(this.socket.onData(data => this._acceptChunk(data)));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.socket.dispose();
|
||||
}
|
||||
|
||||
public onData(listener: (e: VSBuffer) => void): IDisposable {
|
||||
return this._onData.event(listener);
|
||||
}
|
||||
|
||||
public onClose(listener: () => void): IDisposable {
|
||||
return this.socket.onClose(listener);
|
||||
}
|
||||
|
||||
public onEnd(listener: () => void): IDisposable {
|
||||
return this.socket.onEnd(listener);
|
||||
}
|
||||
|
||||
public write(buffer: VSBuffer): void {
|
||||
let headerLen = Constants.MinHeaderByteSize;
|
||||
if (buffer.byteLength < 126) {
|
||||
headerLen += 0;
|
||||
} else if (buffer.byteLength < 2 ** 16) {
|
||||
headerLen += 2;
|
||||
} else {
|
||||
headerLen += 8;
|
||||
}
|
||||
const header = VSBuffer.alloc(headerLen);
|
||||
|
||||
header.writeUInt8(0b10000010, 0);
|
||||
if (buffer.byteLength < 126) {
|
||||
header.writeUInt8(buffer.byteLength, 1);
|
||||
} else if (buffer.byteLength < 2 ** 16) {
|
||||
header.writeUInt8(126, 1);
|
||||
let offset = 1;
|
||||
header.writeUInt8((buffer.byteLength >>> 8) & 0b11111111, ++offset);
|
||||
header.writeUInt8((buffer.byteLength >>> 0) & 0b11111111, ++offset);
|
||||
} else {
|
||||
header.writeUInt8(127, 1);
|
||||
let offset = 1;
|
||||
header.writeUInt8(0, ++offset);
|
||||
header.writeUInt8(0, ++offset);
|
||||
header.writeUInt8(0, ++offset);
|
||||
header.writeUInt8(0, ++offset);
|
||||
header.writeUInt8((buffer.byteLength >>> 24) & 0b11111111, ++offset);
|
||||
header.writeUInt8((buffer.byteLength >>> 16) & 0b11111111, ++offset);
|
||||
header.writeUInt8((buffer.byteLength >>> 8) & 0b11111111, ++offset);
|
||||
header.writeUInt8((buffer.byteLength >>> 0) & 0b11111111, ++offset);
|
||||
}
|
||||
|
||||
this.socket.write(VSBuffer.concat([header, buffer]));
|
||||
}
|
||||
|
||||
public end(): void {
|
||||
this.socket.end();
|
||||
}
|
||||
|
||||
private _acceptChunk(data: VSBuffer): void {
|
||||
if (data.byteLength === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._incomingData.acceptChunk(data);
|
||||
|
||||
while (this._incomingData.byteLength >= this._state.readLen) {
|
||||
|
||||
if (this._state.state === ReadState.PeekHeader) {
|
||||
// peek to see if we can read the entire header
|
||||
const peekHeader = this._incomingData.peek(this._state.readLen);
|
||||
// const firstByte = peekHeader.readUInt8(0);
|
||||
// const finBit = (firstByte & 0b10000000) >>> 7;
|
||||
const secondByte = peekHeader.readUInt8(1);
|
||||
const hasMask = (secondByte & 0b10000000) >>> 7;
|
||||
const len = (secondByte & 0b01111111);
|
||||
|
||||
this._state.state = ReadState.ReadHeader;
|
||||
this._state.readLen = Constants.MinHeaderByteSize + (hasMask ? 4 : 0) + (len === 126 ? 2 : 0) + (len === 127 ? 8 : 0);
|
||||
this._state.mask = 0;
|
||||
|
||||
} else if (this._state.state === ReadState.ReadHeader) {
|
||||
// read entire header
|
||||
const header = this._incomingData.read(this._state.readLen);
|
||||
const secondByte = header.readUInt8(1);
|
||||
const hasMask = (secondByte & 0b10000000) >>> 7;
|
||||
let len = (secondByte & 0b01111111);
|
||||
|
||||
let offset = 1;
|
||||
if (len === 126) {
|
||||
len = (
|
||||
header.readUInt8(++offset) * 2 ** 8
|
||||
+ header.readUInt8(++offset)
|
||||
);
|
||||
} else if (len === 127) {
|
||||
len = (
|
||||
header.readUInt8(++offset) * 0
|
||||
+ header.readUInt8(++offset) * 0
|
||||
+ header.readUInt8(++offset) * 0
|
||||
+ header.readUInt8(++offset) * 0
|
||||
+ header.readUInt8(++offset) * 2 ** 24
|
||||
+ header.readUInt8(++offset) * 2 ** 16
|
||||
+ header.readUInt8(++offset) * 2 ** 8
|
||||
+ header.readUInt8(++offset)
|
||||
);
|
||||
}
|
||||
|
||||
let mask = 0;
|
||||
if (hasMask) {
|
||||
mask = (
|
||||
header.readUInt8(++offset) * 2 ** 24
|
||||
+ header.readUInt8(++offset) * 2 ** 16
|
||||
+ header.readUInt8(++offset) * 2 ** 8
|
||||
+ header.readUInt8(++offset)
|
||||
);
|
||||
}
|
||||
|
||||
this._state.state = ReadState.ReadBody;
|
||||
this._state.readLen = len;
|
||||
this._state.mask = mask;
|
||||
|
||||
} else if (this._state.state === ReadState.ReadBody) {
|
||||
// read body
|
||||
|
||||
const body = this._incomingData.read(this._state.readLen);
|
||||
unmask(body, this._state.mask);
|
||||
|
||||
this._state.state = ReadState.PeekHeader;
|
||||
this._state.readLen = Constants.MinHeaderByteSize;
|
||||
this._state.mask = 0;
|
||||
|
||||
this._onData.fire(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public drain(): Promise<void> {
|
||||
return this.socket.drain();
|
||||
}
|
||||
}
|
||||
|
||||
function unmask(buffer: VSBuffer, mask: number): void {
|
||||
if (mask === 0) {
|
||||
return;
|
||||
}
|
||||
let cnt = buffer.byteLength >>> 2;
|
||||
for (let i = 0; i < cnt; i++) {
|
||||
const v = buffer.readUInt32BE(i * 4);
|
||||
buffer.writeUInt32BE(v ^ mask, i * 4);
|
||||
}
|
||||
let offset = cnt * 4;
|
||||
let bytesLeft = buffer.byteLength - offset;
|
||||
const m3 = (mask >>> 24) & 0b11111111;
|
||||
const m2 = (mask >>> 16) & 0b11111111;
|
||||
const m1 = (mask >>> 8) & 0b11111111;
|
||||
if (bytesLeft >= 1) {
|
||||
buffer.writeUInt8(buffer.readUInt8(offset) ^ m3, offset);
|
||||
}
|
||||
if (bytesLeft >= 2) {
|
||||
buffer.writeUInt8(buffer.readUInt8(offset + 1) ^ m2, offset + 1);
|
||||
}
|
||||
if (bytesLeft >= 3) {
|
||||
buffer.writeUInt8(buffer.readUInt8(offset + 2) ^ m1, offset + 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Read this before there's any chance it is overwritten
|
||||
// Related to https://github.com/microsoft/vscode/issues/30624
|
||||
export const XDG_RUNTIME_DIR = <string | undefined>process.env['XDG_RUNTIME_DIR'];
|
||||
|
||||
const safeIpcPathLengths: { [platform: number]: number } = {
|
||||
[Platform.Linux]: 107,
|
||||
[Platform.Mac]: 103
|
||||
};
|
||||
|
||||
export function createRandomIPCHandle(): string {
|
||||
const randomSuffix = generateUuid();
|
||||
|
||||
// Windows: use named pipe
|
||||
if (process.platform === 'win32') {
|
||||
return `\\\\.\\pipe\\vscode-ipc-${randomSuffix}-sock`;
|
||||
}
|
||||
|
||||
// Mac/Unix: use socket file and prefer
|
||||
// XDG_RUNTIME_DIR over tmpDir
|
||||
let result: string;
|
||||
if (XDG_RUNTIME_DIR) {
|
||||
result = join(XDG_RUNTIME_DIR, `vscode-ipc-${randomSuffix}.sock`);
|
||||
} else {
|
||||
result = join(tmpdir(), `vscode-ipc-${randomSuffix}.sock`);
|
||||
}
|
||||
|
||||
// Validate length
|
||||
validateIPCHandleLength(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function createStaticIPCHandle(directoryPath: string, type: string, version: string): string {
|
||||
const scope = createHash('md5').update(directoryPath).digest('hex');
|
||||
|
||||
// Windows: use named pipe
|
||||
if (process.platform === 'win32') {
|
||||
return `\\\\.\\pipe\\${scope}-${version}-${type}-sock`;
|
||||
}
|
||||
|
||||
// Mac/Unix: use socket file and prefer
|
||||
// XDG_RUNTIME_DIR over user data path
|
||||
// unless portable
|
||||
let result: string;
|
||||
if (XDG_RUNTIME_DIR && !process.env['VSCODE_PORTABLE']) {
|
||||
result = join(XDG_RUNTIME_DIR, `vscode-${scope.substr(0, 8)}-${version}-${type}.sock`);
|
||||
} else {
|
||||
result = join(directoryPath, `${version}-${type}.sock`);
|
||||
}
|
||||
|
||||
// Validate length
|
||||
validateIPCHandleLength(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function validateIPCHandleLength(handle: string): void {
|
||||
const limit = safeIpcPathLengths[platform];
|
||||
if (typeof limit === 'number' && handle.length >= limit) {
|
||||
// https://nodejs.org/api/net.html#net_identifying_paths_for_ipc_connections
|
||||
console.warn(`WARNING: IPC handle "${handle}" is longer than ${limit} chars, try a shorter --user-data-dir`);
|
||||
}
|
||||
}
|
||||
|
||||
export class Server extends IPCServer {
|
||||
|
||||
private static toClientConnectionEvent(server: NetServer): Event<ClientConnectionEvent> {
|
||||
const onConnection = Event.fromNodeEventEmitter<Socket>(server, 'connection');
|
||||
|
||||
return Event.map(onConnection, socket => ({
|
||||
protocol: new Protocol(new NodeSocket(socket)),
|
||||
onDidClientDisconnect: Event.once(Event.fromNodeEventEmitter<void>(socket, 'close'))
|
||||
}));
|
||||
}
|
||||
|
||||
private server: NetServer | null;
|
||||
|
||||
constructor(server: NetServer) {
|
||||
super(Server.toClientConnectionEvent(server));
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
if (this.server) {
|
||||
this.server.close();
|
||||
this.server = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function serve(port: number): Promise<Server>;
|
||||
export function serve(namedPipe: string): Promise<Server>;
|
||||
export function serve(hook: any): Promise<Server> {
|
||||
return new Promise<Server>((c, e) => {
|
||||
const server = createServer();
|
||||
|
||||
server.on('error', e);
|
||||
server.listen(hook, () => {
|
||||
server.removeListener('error', e);
|
||||
c(new Server(server));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function connect(options: { host: string, port: number }, clientId: string): Promise<Client>;
|
||||
export function connect(port: number, clientId: string): Promise<Client>;
|
||||
export function connect(namedPipe: string, clientId: string): Promise<Client>;
|
||||
export function connect(hook: any, clientId: string): Promise<Client> {
|
||||
return new Promise<Client>((c, e) => {
|
||||
const socket = createConnection(hook, () => {
|
||||
socket.removeListener('error', e);
|
||||
c(Client.fromSocket(new NodeSocket(socket), clientId));
|
||||
});
|
||||
|
||||
socket.once('error', e);
|
||||
});
|
||||
}
|
493
lib/vscode/src/vs/base/parts/ipc/test/common/ipc.test.ts
Normal file
493
lib/vscode/src/vs/base/parts/ipc/test/common/ipc.test.ts
Normal file
@ -0,0 +1,493 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IChannel, IServerChannel, IMessagePassingProtocol, IPCServer, ClientConnectionEvent, IPCClient, createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { canceled } from 'vs/base/common/errors';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
|
||||
class QueueProtocol implements IMessagePassingProtocol {
|
||||
|
||||
private buffering = true;
|
||||
private buffers: VSBuffer[] = [];
|
||||
|
||||
private readonly _onMessage = new Emitter<VSBuffer>({
|
||||
onFirstListenerDidAdd: () => {
|
||||
for (const buffer of this.buffers) {
|
||||
this._onMessage.fire(buffer);
|
||||
}
|
||||
|
||||
this.buffers = [];
|
||||
this.buffering = false;
|
||||
},
|
||||
onLastListenerRemove: () => {
|
||||
this.buffering = true;
|
||||
}
|
||||
});
|
||||
|
||||
readonly onMessage = this._onMessage.event;
|
||||
other!: QueueProtocol;
|
||||
|
||||
send(buffer: VSBuffer): void {
|
||||
this.other.receive(buffer);
|
||||
}
|
||||
|
||||
protected receive(buffer: VSBuffer): void {
|
||||
if (this.buffering) {
|
||||
this.buffers.push(buffer);
|
||||
} else {
|
||||
this._onMessage.fire(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createProtocolPair(): [IMessagePassingProtocol, IMessagePassingProtocol] {
|
||||
const one = new QueueProtocol();
|
||||
const other = new QueueProtocol();
|
||||
one.other = other;
|
||||
other.other = one;
|
||||
|
||||
return [one, other];
|
||||
}
|
||||
|
||||
class TestIPCClient extends IPCClient<string> {
|
||||
|
||||
private readonly _onDidDisconnect = new Emitter<void>();
|
||||
readonly onDidDisconnect = this._onDidDisconnect.event;
|
||||
|
||||
constructor(protocol: IMessagePassingProtocol, id: string) {
|
||||
super(protocol, id);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._onDidDisconnect.fire();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class TestIPCServer extends IPCServer<string> {
|
||||
|
||||
private readonly onDidClientConnect: Emitter<ClientConnectionEvent>;
|
||||
|
||||
constructor() {
|
||||
const onDidClientConnect = new Emitter<ClientConnectionEvent>();
|
||||
super(onDidClientConnect.event);
|
||||
this.onDidClientConnect = onDidClientConnect;
|
||||
}
|
||||
|
||||
createConnection(id: string): IPCClient<string> {
|
||||
const [pc, ps] = createProtocolPair();
|
||||
const client = new TestIPCClient(pc, id);
|
||||
|
||||
this.onDidClientConnect.fire({
|
||||
protocol: ps,
|
||||
onDidClientDisconnect: client.onDidDisconnect
|
||||
});
|
||||
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
const TestChannelId = 'testchannel';
|
||||
|
||||
interface ITestService {
|
||||
marco(): Promise<string>;
|
||||
error(message: string): Promise<void>;
|
||||
neverComplete(): Promise<void>;
|
||||
neverCompleteCT(cancellationToken: CancellationToken): Promise<void>;
|
||||
buffersLength(buffers: VSBuffer[]): Promise<number>;
|
||||
marshall(uri: URI): Promise<URI>;
|
||||
context(): Promise<unknown>;
|
||||
|
||||
onPong: Event<string>;
|
||||
}
|
||||
|
||||
class TestService implements ITestService {
|
||||
|
||||
private readonly _onPong = new Emitter<string>();
|
||||
readonly onPong = this._onPong.event;
|
||||
|
||||
marco(): Promise<string> {
|
||||
return Promise.resolve('polo');
|
||||
}
|
||||
|
||||
error(message: string): Promise<void> {
|
||||
return Promise.reject(new Error(message));
|
||||
}
|
||||
|
||||
neverComplete(): Promise<void> {
|
||||
return new Promise(_ => { });
|
||||
}
|
||||
|
||||
neverCompleteCT(cancellationToken: CancellationToken): Promise<void> {
|
||||
if (cancellationToken.isCancellationRequested) {
|
||||
return Promise.reject(canceled());
|
||||
}
|
||||
|
||||
return new Promise((_, e) => cancellationToken.onCancellationRequested(() => e(canceled())));
|
||||
}
|
||||
|
||||
buffersLength(buffers: VSBuffer[]): Promise<number> {
|
||||
return Promise.resolve(buffers.reduce((r, b) => r + b.buffer.length, 0));
|
||||
}
|
||||
|
||||
ping(msg: string): void {
|
||||
this._onPong.fire(msg);
|
||||
}
|
||||
|
||||
marshall(uri: URI): Promise<URI> {
|
||||
return Promise.resolve(uri);
|
||||
}
|
||||
|
||||
context(context?: unknown): Promise<unknown> {
|
||||
return Promise.resolve(context);
|
||||
}
|
||||
}
|
||||
|
||||
class TestChannel implements IServerChannel {
|
||||
|
||||
constructor(private service: ITestService) { }
|
||||
|
||||
call(_: unknown, command: string, arg: any, cancellationToken: CancellationToken): Promise<any> {
|
||||
switch (command) {
|
||||
case 'marco': return this.service.marco();
|
||||
case 'error': return this.service.error(arg);
|
||||
case 'neverComplete': return this.service.neverComplete();
|
||||
case 'neverCompleteCT': return this.service.neverCompleteCT(cancellationToken);
|
||||
case 'buffersLength': return this.service.buffersLength(arg);
|
||||
default: return Promise.reject(new Error('not implemented'));
|
||||
}
|
||||
}
|
||||
|
||||
listen(_: unknown, event: string, arg?: any): Event<any> {
|
||||
switch (event) {
|
||||
case 'onPong': return this.service.onPong;
|
||||
default: throw new Error('not implemented');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TestChannelClient implements ITestService {
|
||||
|
||||
get onPong(): Event<string> {
|
||||
return this.channel.listen('onPong');
|
||||
}
|
||||
|
||||
constructor(private channel: IChannel) { }
|
||||
|
||||
marco(): Promise<string> {
|
||||
return this.channel.call('marco');
|
||||
}
|
||||
|
||||
error(message: string): Promise<void> {
|
||||
return this.channel.call('error', message);
|
||||
}
|
||||
|
||||
neverComplete(): Promise<void> {
|
||||
return this.channel.call('neverComplete');
|
||||
}
|
||||
|
||||
neverCompleteCT(cancellationToken: CancellationToken): Promise<void> {
|
||||
return this.channel.call('neverCompleteCT', undefined, cancellationToken);
|
||||
}
|
||||
|
||||
buffersLength(buffers: VSBuffer[]): Promise<number> {
|
||||
return this.channel.call('buffersLength', buffers);
|
||||
}
|
||||
|
||||
marshall(uri: URI): Promise<URI> {
|
||||
return this.channel.call('marshall', uri);
|
||||
}
|
||||
|
||||
context(): Promise<unknown> {
|
||||
return this.channel.call('context');
|
||||
}
|
||||
}
|
||||
|
||||
suite('Base IPC', function () {
|
||||
|
||||
test('createProtocolPair', async function () {
|
||||
const [clientProtocol, serverProtocol] = createProtocolPair();
|
||||
|
||||
const b1 = VSBuffer.alloc(0);
|
||||
clientProtocol.send(b1);
|
||||
|
||||
const b3 = VSBuffer.alloc(0);
|
||||
serverProtocol.send(b3);
|
||||
|
||||
const b2 = await Event.toPromise(serverProtocol.onMessage);
|
||||
const b4 = await Event.toPromise(clientProtocol.onMessage);
|
||||
|
||||
assert.strictEqual(b1, b2);
|
||||
assert.strictEqual(b3, b4);
|
||||
});
|
||||
|
||||
suite('one to one', function () {
|
||||
let server: IPCServer;
|
||||
let client: IPCClient;
|
||||
let service: TestService;
|
||||
let ipcService: ITestService;
|
||||
|
||||
setup(function () {
|
||||
service = new TestService();
|
||||
const testServer = new TestIPCServer();
|
||||
server = testServer;
|
||||
|
||||
server.registerChannel(TestChannelId, new TestChannel(service));
|
||||
|
||||
client = testServer.createConnection('client1');
|
||||
ipcService = new TestChannelClient(client.getChannel(TestChannelId));
|
||||
});
|
||||
|
||||
teardown(function () {
|
||||
client.dispose();
|
||||
server.dispose();
|
||||
});
|
||||
|
||||
test('call success', async function () {
|
||||
const r = await ipcService.marco();
|
||||
return assert.equal(r, 'polo');
|
||||
});
|
||||
|
||||
test('call error', async function () {
|
||||
try {
|
||||
await ipcService.error('nice error');
|
||||
return assert.fail('should not reach here');
|
||||
} catch (err) {
|
||||
return assert.equal(err.message, 'nice error');
|
||||
}
|
||||
});
|
||||
|
||||
test('cancel call with cancelled cancellation token', async function () {
|
||||
try {
|
||||
await ipcService.neverCompleteCT(CancellationToken.Cancelled);
|
||||
return assert.fail('should not reach here');
|
||||
} catch (err) {
|
||||
return assert(err.message === 'Canceled');
|
||||
}
|
||||
});
|
||||
|
||||
test('cancel call with cancellation token (sync)', function () {
|
||||
const cts = new CancellationTokenSource();
|
||||
const promise = ipcService.neverCompleteCT(cts.token).then(
|
||||
_ => assert.fail('should not reach here'),
|
||||
err => assert(err.message === 'Canceled')
|
||||
);
|
||||
|
||||
cts.cancel();
|
||||
|
||||
return promise;
|
||||
});
|
||||
|
||||
test('cancel call with cancellation token (async)', function () {
|
||||
const cts = new CancellationTokenSource();
|
||||
const promise = ipcService.neverCompleteCT(cts.token).then(
|
||||
_ => assert.fail('should not reach here'),
|
||||
err => assert(err.message === 'Canceled')
|
||||
);
|
||||
|
||||
setTimeout(() => cts.cancel());
|
||||
|
||||
return promise;
|
||||
});
|
||||
|
||||
test('listen to events', async function () {
|
||||
const messages: string[] = [];
|
||||
|
||||
ipcService.onPong(msg => messages.push(msg));
|
||||
await timeout(0);
|
||||
|
||||
assert.deepEqual(messages, []);
|
||||
service.ping('hello');
|
||||
await timeout(0);
|
||||
|
||||
assert.deepEqual(messages, ['hello']);
|
||||
service.ping('world');
|
||||
await timeout(0);
|
||||
|
||||
assert.deepEqual(messages, ['hello', 'world']);
|
||||
});
|
||||
|
||||
test('buffers in arrays', async function () {
|
||||
const r = await ipcService.buffersLength([VSBuffer.alloc(2), VSBuffer.alloc(3)]);
|
||||
return assert.equal(r, 5);
|
||||
});
|
||||
});
|
||||
|
||||
suite('one to one (proxy)', function () {
|
||||
let server: IPCServer;
|
||||
let client: IPCClient;
|
||||
let service: TestService;
|
||||
let ipcService: ITestService;
|
||||
|
||||
setup(function () {
|
||||
service = new TestService();
|
||||
const testServer = new TestIPCServer();
|
||||
server = testServer;
|
||||
|
||||
server.registerChannel(TestChannelId, createChannelReceiver(service));
|
||||
|
||||
client = testServer.createConnection('client1');
|
||||
ipcService = createChannelSender(client.getChannel(TestChannelId));
|
||||
});
|
||||
|
||||
teardown(function () {
|
||||
client.dispose();
|
||||
server.dispose();
|
||||
});
|
||||
|
||||
test('call success', async function () {
|
||||
const r = await ipcService.marco();
|
||||
return assert.equal(r, 'polo');
|
||||
});
|
||||
|
||||
test('call error', async function () {
|
||||
try {
|
||||
await ipcService.error('nice error');
|
||||
return assert.fail('should not reach here');
|
||||
} catch (err) {
|
||||
return assert.equal(err.message, 'nice error');
|
||||
}
|
||||
});
|
||||
|
||||
test('listen to events', async function () {
|
||||
const messages: string[] = [];
|
||||
|
||||
ipcService.onPong(msg => messages.push(msg));
|
||||
await timeout(0);
|
||||
|
||||
assert.deepEqual(messages, []);
|
||||
service.ping('hello');
|
||||
await timeout(0);
|
||||
|
||||
assert.deepEqual(messages, ['hello']);
|
||||
service.ping('world');
|
||||
await timeout(0);
|
||||
|
||||
assert.deepEqual(messages, ['hello', 'world']);
|
||||
});
|
||||
|
||||
test('marshalling uri', async function () {
|
||||
const uri = URI.file('foobar');
|
||||
const r = await ipcService.marshall(uri);
|
||||
assert.ok(r instanceof URI);
|
||||
return assert.ok(isEqual(r, uri));
|
||||
});
|
||||
|
||||
test('buffers in arrays', async function () {
|
||||
const r = await ipcService.buffersLength([VSBuffer.alloc(2), VSBuffer.alloc(3)]);
|
||||
return assert.equal(r, 5);
|
||||
});
|
||||
});
|
||||
|
||||
suite('one to one (proxy, extra context)', function () {
|
||||
let server: IPCServer;
|
||||
let client: IPCClient;
|
||||
let service: TestService;
|
||||
let ipcService: ITestService;
|
||||
|
||||
setup(function () {
|
||||
service = new TestService();
|
||||
const testServer = new TestIPCServer();
|
||||
server = testServer;
|
||||
|
||||
server.registerChannel(TestChannelId, createChannelReceiver(service));
|
||||
|
||||
client = testServer.createConnection('client1');
|
||||
ipcService = createChannelSender(client.getChannel(TestChannelId), { context: 'Super Context' });
|
||||
});
|
||||
|
||||
teardown(function () {
|
||||
client.dispose();
|
||||
server.dispose();
|
||||
});
|
||||
|
||||
test('call extra context', async function () {
|
||||
const r = await ipcService.context();
|
||||
return assert.equal(r, 'Super Context');
|
||||
});
|
||||
});
|
||||
|
||||
suite('one to many', function () {
|
||||
test('all clients get pinged', async function () {
|
||||
const service = new TestService();
|
||||
const channel = new TestChannel(service);
|
||||
const server = new TestIPCServer();
|
||||
server.registerChannel('channel', channel);
|
||||
|
||||
let client1GotPinged = false;
|
||||
const client1 = server.createConnection('client1');
|
||||
const ipcService1 = new TestChannelClient(client1.getChannel('channel'));
|
||||
ipcService1.onPong(() => client1GotPinged = true);
|
||||
|
||||
let client2GotPinged = false;
|
||||
const client2 = server.createConnection('client2');
|
||||
const ipcService2 = new TestChannelClient(client2.getChannel('channel'));
|
||||
ipcService2.onPong(() => client2GotPinged = true);
|
||||
|
||||
await timeout(1);
|
||||
service.ping('hello');
|
||||
|
||||
await timeout(1);
|
||||
assert(client1GotPinged, 'client 1 got pinged');
|
||||
assert(client2GotPinged, 'client 2 got pinged');
|
||||
|
||||
client1.dispose();
|
||||
client2.dispose();
|
||||
server.dispose();
|
||||
});
|
||||
|
||||
test('server gets pings from all clients (broadcast channel)', async function () {
|
||||
const server = new TestIPCServer();
|
||||
|
||||
const client1 = server.createConnection('client1');
|
||||
const clientService1 = new TestService();
|
||||
const clientChannel1 = new TestChannel(clientService1);
|
||||
client1.registerChannel('channel', clientChannel1);
|
||||
|
||||
const pings: string[] = [];
|
||||
const channel = server.getChannel('channel', () => true);
|
||||
const service = new TestChannelClient(channel);
|
||||
service.onPong(msg => pings.push(msg));
|
||||
|
||||
await timeout(1);
|
||||
clientService1.ping('hello 1');
|
||||
|
||||
await timeout(1);
|
||||
assert.deepEqual(pings, ['hello 1']);
|
||||
|
||||
const client2 = server.createConnection('client2');
|
||||
const clientService2 = new TestService();
|
||||
const clientChannel2 = new TestChannel(clientService2);
|
||||
client2.registerChannel('channel', clientChannel2);
|
||||
|
||||
await timeout(1);
|
||||
clientService2.ping('hello 2');
|
||||
|
||||
await timeout(1);
|
||||
assert.deepEqual(pings, ['hello 1', 'hello 2']);
|
||||
|
||||
client1.dispose();
|
||||
clientService1.ping('hello 1');
|
||||
|
||||
await timeout(1);
|
||||
assert.deepEqual(pings, ['hello 1', 'hello 2']);
|
||||
|
||||
await timeout(1);
|
||||
clientService2.ping('hello again 2');
|
||||
|
||||
await timeout(1);
|
||||
assert.deepEqual(pings, ['hello 1', 'hello 2', 'hello again 2']);
|
||||
|
||||
client2.dispose();
|
||||
server.dispose();
|
||||
});
|
||||
});
|
||||
});
|
78
lib/vscode/src/vs/base/parts/ipc/test/node/ipc.cp.test.ts
Normal file
78
lib/vscode/src/vs/base/parts/ipc/test/node/ipc.cp.test.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { TestServiceClient } from './testService';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
|
||||
function createClient(): Client {
|
||||
return new Client(getPathFromAmdModule(require, 'bootstrap-fork'), {
|
||||
serverName: 'TestServer',
|
||||
env: { AMD_ENTRYPOINT: 'vs/base/parts/ipc/test/node/testApp', verbose: true }
|
||||
});
|
||||
}
|
||||
|
||||
suite('IPC, Child Process', () => {
|
||||
test('createChannel', () => {
|
||||
const client = createClient();
|
||||
const channel = client.getChannel('test');
|
||||
const service = new TestServiceClient(channel);
|
||||
|
||||
const result = service.pong('ping').then(r => {
|
||||
assert.equal(r.incoming, 'ping');
|
||||
assert.equal(r.outgoing, 'pong');
|
||||
});
|
||||
|
||||
return result.finally(() => client.dispose());
|
||||
});
|
||||
|
||||
test('events', () => {
|
||||
const client = createClient();
|
||||
const channel = client.getChannel('test');
|
||||
const service = new TestServiceClient(channel);
|
||||
|
||||
const event = new Promise((c, e) => {
|
||||
service.onMarco(({ answer }) => {
|
||||
try {
|
||||
assert.equal(answer, 'polo');
|
||||
c(undefined);
|
||||
} catch (err) {
|
||||
e(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const request = service.marco();
|
||||
const result = Promise.all([request, event]);
|
||||
|
||||
return result.finally(() => client.dispose());
|
||||
});
|
||||
|
||||
test('event dispose', () => {
|
||||
const client = createClient();
|
||||
const channel = client.getChannel('test');
|
||||
const service = new TestServiceClient(channel);
|
||||
|
||||
let count = 0;
|
||||
const disposable = service.onMarco(() => count++);
|
||||
|
||||
const result = service.marco().then(async answer => {
|
||||
assert.equal(answer, 'polo');
|
||||
assert.equal(count, 1);
|
||||
|
||||
const answer_1 = await service.marco();
|
||||
assert.equal(answer_1, 'polo');
|
||||
assert.equal(count, 2);
|
||||
disposable.dispose();
|
||||
|
||||
const answer_2 = await service.marco();
|
||||
assert.equal(answer_2, 'polo');
|
||||
assert.equal(count, 2);
|
||||
});
|
||||
|
||||
return result.finally(() => client.dispose());
|
||||
});
|
||||
});
|
258
lib/vscode/src/vs/base/parts/ipc/test/node/ipc.net.test.ts
Normal file
258
lib/vscode/src/vs/base/parts/ipc/test/node/ipc.net.test.ts
Normal file
@ -0,0 +1,258 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { createServer, Socket } from 'net';
|
||||
import { EventEmitter } from 'events';
|
||||
import { Protocol, PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net';
|
||||
import { createRandomIPCHandle, createStaticIPCHandle, NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { tmpdir } from 'os';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
|
||||
class MessageStream {
|
||||
|
||||
private _currentComplete: ((data: VSBuffer) => void) | null;
|
||||
private _messages: VSBuffer[];
|
||||
|
||||
constructor(x: Protocol | PersistentProtocol) {
|
||||
this._currentComplete = null;
|
||||
this._messages = [];
|
||||
x.onMessage(data => {
|
||||
this._messages.push(data);
|
||||
this._trigger();
|
||||
});
|
||||
}
|
||||
|
||||
private _trigger(): void {
|
||||
if (!this._currentComplete) {
|
||||
return;
|
||||
}
|
||||
if (this._messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
const complete = this._currentComplete;
|
||||
const msg = this._messages.shift()!;
|
||||
|
||||
this._currentComplete = null;
|
||||
complete(msg);
|
||||
}
|
||||
|
||||
public waitForOne(): Promise<VSBuffer> {
|
||||
return new Promise<VSBuffer>((complete) => {
|
||||
this._currentComplete = complete;
|
||||
this._trigger();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class EtherStream extends EventEmitter {
|
||||
constructor(
|
||||
private readonly _ether: Ether,
|
||||
private readonly _name: 'a' | 'b'
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
write(data: Buffer, cb?: Function): boolean {
|
||||
if (!Buffer.isBuffer(data)) {
|
||||
throw new Error(`Invalid data`);
|
||||
}
|
||||
this._ether.write(this._name, data);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class Ether {
|
||||
|
||||
private readonly _a: EtherStream;
|
||||
private readonly _b: EtherStream;
|
||||
|
||||
private _ab: Buffer[];
|
||||
private _ba: Buffer[];
|
||||
|
||||
public get a(): Socket {
|
||||
return <any>this._a;
|
||||
}
|
||||
|
||||
public get b(): Socket {
|
||||
return <any>this._b;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this._a = new EtherStream(this, 'a');
|
||||
this._b = new EtherStream(this, 'b');
|
||||
this._ab = [];
|
||||
this._ba = [];
|
||||
}
|
||||
|
||||
public write(from: 'a' | 'b', data: Buffer): void {
|
||||
if (from === 'a') {
|
||||
this._ab.push(data);
|
||||
} else {
|
||||
this._ba.push(data);
|
||||
}
|
||||
|
||||
setImmediate(() => this._deliver());
|
||||
}
|
||||
|
||||
private _deliver(): void {
|
||||
|
||||
if (this._ab.length > 0) {
|
||||
const data = Buffer.concat(this._ab);
|
||||
this._ab.length = 0;
|
||||
this._b.emit('data', data);
|
||||
setImmediate(() => this._deliver());
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._ba.length > 0) {
|
||||
const data = Buffer.concat(this._ba);
|
||||
this._ba.length = 0;
|
||||
this._a.emit('data', data);
|
||||
setImmediate(() => this._deliver());
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
suite('IPC, Socket Protocol', () => {
|
||||
|
||||
let ether: Ether;
|
||||
|
||||
setup(() => {
|
||||
ether = new Ether();
|
||||
});
|
||||
|
||||
test('read/write', async () => {
|
||||
|
||||
const a = new Protocol(new NodeSocket(ether.a));
|
||||
const b = new Protocol(new NodeSocket(ether.b));
|
||||
const bMessages = new MessageStream(b);
|
||||
|
||||
a.send(VSBuffer.fromString('foobarfarboo'));
|
||||
const msg1 = await bMessages.waitForOne();
|
||||
assert.equal(msg1.toString(), 'foobarfarboo');
|
||||
|
||||
const buffer = VSBuffer.alloc(1);
|
||||
buffer.writeUInt8(123, 0);
|
||||
a.send(buffer);
|
||||
const msg2 = await bMessages.waitForOne();
|
||||
assert.equal(msg2.readUInt8(0), 123);
|
||||
});
|
||||
|
||||
|
||||
test('read/write, object data', async () => {
|
||||
|
||||
const a = new Protocol(new NodeSocket(ether.a));
|
||||
const b = new Protocol(new NodeSocket(ether.b));
|
||||
const bMessages = new MessageStream(b);
|
||||
|
||||
const data = {
|
||||
pi: Math.PI,
|
||||
foo: 'bar',
|
||||
more: true,
|
||||
data: 'Hello World'.split('')
|
||||
};
|
||||
|
||||
a.send(VSBuffer.fromString(JSON.stringify(data)));
|
||||
const msg = await bMessages.waitForOne();
|
||||
assert.deepEqual(JSON.parse(msg.toString()), data);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('PersistentProtocol reconnection', () => {
|
||||
let ether: Ether;
|
||||
|
||||
setup(() => {
|
||||
ether = new Ether();
|
||||
});
|
||||
|
||||
test('acks get piggybacked with messages', async () => {
|
||||
const a = new PersistentProtocol(new NodeSocket(ether.a));
|
||||
const aMessages = new MessageStream(a);
|
||||
const b = new PersistentProtocol(new NodeSocket(ether.b));
|
||||
const bMessages = new MessageStream(b);
|
||||
|
||||
a.send(VSBuffer.fromString('a1'));
|
||||
assert.equal(a.unacknowledgedCount, 1);
|
||||
assert.equal(b.unacknowledgedCount, 0);
|
||||
|
||||
a.send(VSBuffer.fromString('a2'));
|
||||
assert.equal(a.unacknowledgedCount, 2);
|
||||
assert.equal(b.unacknowledgedCount, 0);
|
||||
|
||||
a.send(VSBuffer.fromString('a3'));
|
||||
assert.equal(a.unacknowledgedCount, 3);
|
||||
assert.equal(b.unacknowledgedCount, 0);
|
||||
|
||||
const a1 = await bMessages.waitForOne();
|
||||
assert.equal(a1.toString(), 'a1');
|
||||
assert.equal(a.unacknowledgedCount, 3);
|
||||
assert.equal(b.unacknowledgedCount, 0);
|
||||
|
||||
const a2 = await bMessages.waitForOne();
|
||||
assert.equal(a2.toString(), 'a2');
|
||||
assert.equal(a.unacknowledgedCount, 3);
|
||||
assert.equal(b.unacknowledgedCount, 0);
|
||||
|
||||
const a3 = await bMessages.waitForOne();
|
||||
assert.equal(a3.toString(), 'a3');
|
||||
assert.equal(a.unacknowledgedCount, 3);
|
||||
assert.equal(b.unacknowledgedCount, 0);
|
||||
|
||||
b.send(VSBuffer.fromString('b1'));
|
||||
assert.equal(a.unacknowledgedCount, 3);
|
||||
assert.equal(b.unacknowledgedCount, 1);
|
||||
|
||||
const b1 = await aMessages.waitForOne();
|
||||
assert.equal(b1.toString(), 'b1');
|
||||
assert.equal(a.unacknowledgedCount, 0);
|
||||
assert.equal(b.unacknowledgedCount, 1);
|
||||
|
||||
a.send(VSBuffer.fromString('a4'));
|
||||
assert.equal(a.unacknowledgedCount, 1);
|
||||
assert.equal(b.unacknowledgedCount, 1);
|
||||
|
||||
const b2 = await bMessages.waitForOne();
|
||||
assert.equal(b2.toString(), 'a4');
|
||||
assert.equal(a.unacknowledgedCount, 1);
|
||||
assert.equal(b.unacknowledgedCount, 0);
|
||||
});
|
||||
});
|
||||
|
||||
suite('IPC, create handle', () => {
|
||||
|
||||
test('createRandomIPCHandle', async () => {
|
||||
return testIPCHandle(createRandomIPCHandle());
|
||||
});
|
||||
|
||||
test('createStaticIPCHandle', async () => {
|
||||
return testIPCHandle(createStaticIPCHandle(tmpdir(), 'test', product.version));
|
||||
});
|
||||
|
||||
function testIPCHandle(handle: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const pipeName = createRandomIPCHandle();
|
||||
|
||||
const server = createServer();
|
||||
|
||||
server.on('error', () => {
|
||||
return new Promise(() => server.close(() => reject()));
|
||||
});
|
||||
|
||||
server.listen(pipeName, () => {
|
||||
server.removeListener('error', reject);
|
||||
|
||||
return new Promise(() => {
|
||||
server.close(() => resolve());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
});
|
11
lib/vscode/src/vs/base/parts/ipc/test/node/testApp.ts
Normal file
11
lib/vscode/src/vs/base/parts/ipc/test/node/testApp.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { TestChannel, TestService } from './testService';
|
||||
|
||||
const server = new Server('test');
|
||||
const service = new TestService();
|
||||
server.registerChannel('test', new TestChannel(service));
|
79
lib/vscode/src/vs/base/parts/ipc/test/node/testService.ts
Normal file
79
lib/vscode/src/vs/base/parts/ipc/test/node/testService.ts
Normal file
@ -0,0 +1,79 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
|
||||
export interface IMarcoPoloEvent {
|
||||
answer: string;
|
||||
}
|
||||
|
||||
export interface ITestService {
|
||||
onMarco: Event<IMarcoPoloEvent>;
|
||||
marco(): Promise<string>;
|
||||
pong(ping: string): Promise<{ incoming: string, outgoing: string }>;
|
||||
cancelMe(): Promise<boolean>;
|
||||
}
|
||||
|
||||
export class TestService implements ITestService {
|
||||
|
||||
private readonly _onMarco = new Emitter<IMarcoPoloEvent>();
|
||||
onMarco: Event<IMarcoPoloEvent> = this._onMarco.event;
|
||||
|
||||
marco(): Promise<string> {
|
||||
this._onMarco.fire({ answer: 'polo' });
|
||||
return Promise.resolve('polo');
|
||||
}
|
||||
|
||||
pong(ping: string): Promise<{ incoming: string, outgoing: string }> {
|
||||
return Promise.resolve({ incoming: ping, outgoing: 'pong' });
|
||||
}
|
||||
|
||||
cancelMe(): Promise<boolean> {
|
||||
return Promise.resolve(timeout(100)).then(() => true);
|
||||
}
|
||||
}
|
||||
|
||||
export class TestChannel implements IServerChannel {
|
||||
|
||||
constructor(private testService: ITestService) { }
|
||||
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
switch (event) {
|
||||
case 'marco': return this.testService.onMarco;
|
||||
}
|
||||
|
||||
throw new Error('Event not found');
|
||||
}
|
||||
|
||||
call(_: unknown, command: string, ...args: any[]): Promise<any> {
|
||||
switch (command) {
|
||||
case 'pong': return this.testService.pong(args[0]);
|
||||
case 'cancelMe': return this.testService.cancelMe();
|
||||
case 'marco': return this.testService.marco();
|
||||
default: return Promise.reject(new Error(`command not found: ${command}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class TestServiceClient implements ITestService {
|
||||
|
||||
get onMarco(): Event<IMarcoPoloEvent> { return this.channel.listen('marco'); }
|
||||
|
||||
constructor(private channel: IChannel) { }
|
||||
|
||||
marco(): Promise<string> {
|
||||
return this.channel.call('marco');
|
||||
}
|
||||
|
||||
pong(ping: string): Promise<{ incoming: string, outgoing: string }> {
|
||||
return this.channel.call('pong', ping);
|
||||
}
|
||||
|
||||
cancelMe(): Promise<boolean> {
|
||||
return this.channel.call('cancelMe');
|
||||
}
|
||||
}
|
@ -0,0 +1,280 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.quick-input-widget {
|
||||
position: absolute;
|
||||
width: 600px;
|
||||
z-index: 2000;
|
||||
padding-bottom: 6px;
|
||||
left: 50%;
|
||||
margin-left: -300px;
|
||||
}
|
||||
|
||||
.quick-input-titlebar {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.quick-input-left-action-bar {
|
||||
display: flex;
|
||||
margin-left: 4px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.quick-input-left-action-bar.monaco-action-bar .actions-container {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.quick-input-title {
|
||||
padding: 3px 0px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quick-input-right-action-bar {
|
||||
display: flex;
|
||||
margin-right: 4px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.quick-input-titlebar .monaco-action-bar .action-label.codicon {
|
||||
margin: 0;
|
||||
width: 19px;
|
||||
height: 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.quick-input-description {
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
.quick-input-header .quick-input-description {
|
||||
margin: 4px 2px;
|
||||
}
|
||||
|
||||
.quick-input-header {
|
||||
display: flex;
|
||||
padding: 6px 6px 0px 6px;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.quick-input-widget.hidden-input .quick-input-header {
|
||||
/* reduce margins and paddings when input box hidden */
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.quick-input-and-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quick-input-check-all {
|
||||
align-self: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.quick-input-filter {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quick-input-box {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.quick-input-widget.show-checkboxes .quick-input-box,
|
||||
.quick-input-widget.show-checkboxes .quick-input-message {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.quick-input-visible-count {
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
}
|
||||
|
||||
.quick-input-count {
|
||||
align-self: center;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.quick-input-count .monaco-count-badge {
|
||||
vertical-align: middle;
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
min-height: auto;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.quick-input-action {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.quick-input-action .monaco-text-button {
|
||||
font-size: 11px;
|
||||
padding: 0 6px;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.quick-input-message {
|
||||
margin-top: -1px;
|
||||
padding: 5px 5px 2px 5px;
|
||||
}
|
||||
|
||||
.quick-input-progress.monaco-progress-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quick-input-progress.monaco-progress-container,
|
||||
.quick-input-progress.monaco-progress-container .progress-bit {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.quick-input-list {
|
||||
line-height: 22px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.quick-input-widget.hidden-input .quick-input-list {
|
||||
margin-top: 0; /* reduce margins when input box hidden */
|
||||
}
|
||||
|
||||
.quick-input-list .monaco-list {
|
||||
overflow: hidden;
|
||||
max-height: calc(20 * 22px);
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry {
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry.quick-input-list-separator-border {
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
}
|
||||
|
||||
.quick-input-list .monaco-list-row:first-child .quick-input-list-entry.quick-input-list-separator-border {
|
||||
border-top-style: none;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-label {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-checkbox {
|
||||
align-self: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-rows {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.quick-input-widget.show-checkboxes .quick-input-list .quick-input-list-rows {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.quick-input-widget .quick-input-list .quick-input-list-checkbox {
|
||||
display: none;
|
||||
}
|
||||
.quick-input-widget.show-checkboxes .quick-input-list .quick-input-list-checkbox {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-rows > .quick-input-list-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-rows > .quick-input-list-row .monaco-icon-label,
|
||||
.quick-input-list .quick-input-list-rows > .quick-input-list-row .monaco-icon-label .monaco-icon-label-container > .monaco-icon-name-container {
|
||||
flex: 1; /* make sure the icon label grows within the row */
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-rows > .quick-input-list-row .codicon[class*='codicon-'] {
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-rows .monaco-highlighted-label span {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry .quick-input-list-entry-keybinding {
|
||||
margin-right: 8px; /* separate from the separator label or scrollbar if any */
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-label-meta {
|
||||
opacity: 0.7;
|
||||
line-height: normal;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quick-input-list .monaco-highlighted-label .highlight {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry .quick-input-list-separator {
|
||||
margin-right: 8px; /* separate from keybindings or actions */
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry-action-bar {
|
||||
display: flex;
|
||||
flex: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry-action-bar .action-label {
|
||||
/*
|
||||
* By default, actions in the quick input action bar are hidden
|
||||
* until hovered over them or selected.
|
||||
*/
|
||||
display: none;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry-action-bar .action-label.codicon {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
padding: 0 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry-action-bar {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry-action-bar {
|
||||
margin-right: 4px; /* separate from scrollbar */
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry-action-bar .action-label.codicon {
|
||||
margin-right: 4px; /* separate actions */
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry .quick-input-list-entry-action-bar .action-label.always-visible,
|
||||
.quick-input-list .quick-input-list-entry:hover .quick-input-list-entry-action-bar .action-label,
|
||||
.quick-input-list .monaco-list-row.focused .quick-input-list-entry-action-bar .action-label {
|
||||
display: flex;
|
||||
}
|
1728
lib/vscode/src/vs/base/parts/quickinput/browser/quickInput.ts
Normal file
1728
lib/vscode/src/vs/base/parts/quickinput/browser/quickInput.ts
Normal file
File diff suppressed because it is too large
Load Diff
128
lib/vscode/src/vs/base/parts/quickinput/browser/quickInputBox.ts
Normal file
128
lib/vscode/src/vs/base/parts/quickinput/browser/quickInputBox.ts
Normal file
@ -0,0 +1,128 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/quickInput';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { InputBox, IRange, MessageType, IInputBoxStyles } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
export class QuickInputBox extends Disposable {
|
||||
|
||||
private container: HTMLElement;
|
||||
private inputBox: InputBox;
|
||||
|
||||
constructor(
|
||||
private parent: HTMLElement
|
||||
) {
|
||||
super();
|
||||
this.container = dom.append(this.parent, $('.quick-input-box'));
|
||||
this.inputBox = this._register(new InputBox(this.container, undefined));
|
||||
}
|
||||
|
||||
onKeyDown = (handler: (event: StandardKeyboardEvent) => void): IDisposable => {
|
||||
return dom.addDisposableListener(this.inputBox.inputElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
|
||||
handler(new StandardKeyboardEvent(e));
|
||||
});
|
||||
};
|
||||
|
||||
onMouseDown = (handler: (event: StandardMouseEvent) => void): IDisposable => {
|
||||
return dom.addDisposableListener(this.inputBox.inputElement, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => {
|
||||
handler(new StandardMouseEvent(e));
|
||||
});
|
||||
};
|
||||
|
||||
onDidChange = (handler: (event: string) => void): IDisposable => {
|
||||
return this.inputBox.onDidChange(handler);
|
||||
};
|
||||
|
||||
get value() {
|
||||
return this.inputBox.value;
|
||||
}
|
||||
|
||||
set value(value: string) {
|
||||
this.inputBox.value = value;
|
||||
}
|
||||
|
||||
select(range: IRange | null = null): void {
|
||||
this.inputBox.select(range);
|
||||
}
|
||||
|
||||
isSelectionAtEnd(): boolean {
|
||||
return this.inputBox.isSelectionAtEnd();
|
||||
}
|
||||
|
||||
setPlaceholder(placeholder: string): void {
|
||||
this.inputBox.setPlaceHolder(placeholder);
|
||||
}
|
||||
|
||||
get placeholder() {
|
||||
return this.inputBox.inputElement.getAttribute('placeholder') || '';
|
||||
}
|
||||
|
||||
set placeholder(placeholder: string) {
|
||||
this.inputBox.setPlaceHolder(placeholder);
|
||||
}
|
||||
|
||||
get ariaLabel() {
|
||||
return this.inputBox.getAriaLabel();
|
||||
}
|
||||
|
||||
set ariaLabel(ariaLabel: string) {
|
||||
this.inputBox.setAriaLabel(ariaLabel);
|
||||
}
|
||||
|
||||
get password() {
|
||||
return this.inputBox.inputElement.type === 'password';
|
||||
}
|
||||
|
||||
set password(password: boolean) {
|
||||
this.inputBox.inputElement.type = password ? 'password' : 'text';
|
||||
}
|
||||
|
||||
set enabled(enabled: boolean) {
|
||||
this.inputBox.setEnabled(enabled);
|
||||
}
|
||||
|
||||
hasFocus(): boolean {
|
||||
return this.inputBox.hasFocus();
|
||||
}
|
||||
|
||||
setAttribute(name: string, value: string): void {
|
||||
this.inputBox.inputElement.setAttribute(name, value);
|
||||
}
|
||||
|
||||
removeAttribute(name: string): void {
|
||||
this.inputBox.inputElement.removeAttribute(name);
|
||||
}
|
||||
|
||||
showDecoration(decoration: Severity): void {
|
||||
if (decoration === Severity.Ignore) {
|
||||
this.inputBox.hideMessage();
|
||||
} else {
|
||||
this.inputBox.showMessage({ type: decoration === Severity.Info ? MessageType.INFO : decoration === Severity.Warning ? MessageType.WARNING : MessageType.ERROR, content: '' });
|
||||
}
|
||||
}
|
||||
|
||||
stylesForType(decoration: Severity) {
|
||||
return this.inputBox.stylesForType(decoration === Severity.Info ? MessageType.INFO : decoration === Severity.Warning ? MessageType.WARNING : MessageType.ERROR);
|
||||
}
|
||||
|
||||
setFocus(): void {
|
||||
this.inputBox.focus();
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
this.inputBox.layout();
|
||||
}
|
||||
|
||||
style(styles: IInputBoxStyles): void {
|
||||
this.inputBox.style(styles);
|
||||
}
|
||||
}
|
@ -0,0 +1,726 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/quickInput';
|
||||
import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator } from 'vs/base/parts/quickinput/common/quickInput';
|
||||
import { IMatch } from 'vs/base/common/filters';
|
||||
import { matchesFuzzyCodiconAware, parseCodicons } from 'vs/base/common/codicon';
|
||||
import { compareAnything } from 'vs/base/common/comparers';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel';
|
||||
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { range } from 'vs/base/common/arrays';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { getIconClass } from 'vs/base/parts/quickinput/browser/quickInputUtils';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
import { IQuickInputOptions } from 'vs/base/parts/quickinput/browser/quickInput';
|
||||
import { IListOptions, List, IListStyles, IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
interface IListElement {
|
||||
readonly index: number;
|
||||
readonly item: IQuickPickItem;
|
||||
readonly saneLabel: string;
|
||||
readonly saneAriaLabel: string;
|
||||
readonly saneDescription?: string;
|
||||
readonly saneDetail?: string;
|
||||
readonly labelHighlights?: IMatch[];
|
||||
readonly descriptionHighlights?: IMatch[];
|
||||
readonly detailHighlights?: IMatch[];
|
||||
readonly checked: boolean;
|
||||
readonly separator?: IQuickPickSeparator;
|
||||
readonly fireButtonTriggered: (event: IQuickPickItemButtonEvent<IQuickPickItem>) => void;
|
||||
}
|
||||
|
||||
class ListElement implements IListElement, IDisposable {
|
||||
index!: number;
|
||||
item!: IQuickPickItem;
|
||||
saneLabel!: string;
|
||||
saneAriaLabel!: string;
|
||||
saneDescription?: string;
|
||||
saneDetail?: string;
|
||||
hidden = false;
|
||||
private readonly _onChecked = new Emitter<boolean>();
|
||||
onChecked = this._onChecked.event;
|
||||
_checked?: boolean;
|
||||
get checked() {
|
||||
return !!this._checked;
|
||||
}
|
||||
set checked(value: boolean) {
|
||||
if (value !== this._checked) {
|
||||
this._checked = value;
|
||||
this._onChecked.fire(value);
|
||||
}
|
||||
}
|
||||
separator?: IQuickPickSeparator;
|
||||
labelHighlights?: IMatch[];
|
||||
descriptionHighlights?: IMatch[];
|
||||
detailHighlights?: IMatch[];
|
||||
fireButtonTriggered!: (event: IQuickPickItemButtonEvent<IQuickPickItem>) => void;
|
||||
|
||||
constructor(init: IListElement) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._onChecked.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
interface IListElementTemplateData {
|
||||
entry: HTMLDivElement;
|
||||
checkbox: HTMLInputElement;
|
||||
label: IconLabel;
|
||||
keybinding: KeybindingLabel;
|
||||
detail: HighlightedLabel;
|
||||
separator: HTMLDivElement;
|
||||
actionBar: ActionBar;
|
||||
element: ListElement;
|
||||
toDisposeElement: IDisposable[];
|
||||
toDisposeTemplate: IDisposable[];
|
||||
}
|
||||
|
||||
class ListElementRenderer implements IListRenderer<ListElement, IListElementTemplateData> {
|
||||
|
||||
static readonly ID = 'listelement';
|
||||
|
||||
get templateId() {
|
||||
return ListElementRenderer.ID;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): IListElementTemplateData {
|
||||
const data: IListElementTemplateData = Object.create(null);
|
||||
data.toDisposeElement = [];
|
||||
data.toDisposeTemplate = [];
|
||||
|
||||
data.entry = dom.append(container, $('.quick-input-list-entry'));
|
||||
|
||||
// Checkbox
|
||||
const label = dom.append(data.entry, $('label.quick-input-list-label'));
|
||||
data.toDisposeTemplate.push(dom.addStandardDisposableListener(label, dom.EventType.CLICK, e => {
|
||||
if (!data.checkbox.offsetParent) { // If checkbox not visible:
|
||||
e.preventDefault(); // Prevent toggle of checkbox when it is immediately shown afterwards. #91740
|
||||
}
|
||||
}));
|
||||
data.checkbox = <HTMLInputElement>dom.append(label, $('input.quick-input-list-checkbox'));
|
||||
data.checkbox.type = 'checkbox';
|
||||
data.toDisposeTemplate.push(dom.addStandardDisposableListener(data.checkbox, dom.EventType.CHANGE, e => {
|
||||
data.element.checked = data.checkbox.checked;
|
||||
}));
|
||||
|
||||
// Rows
|
||||
const rows = dom.append(label, $('.quick-input-list-rows'));
|
||||
const row1 = dom.append(rows, $('.quick-input-list-row'));
|
||||
const row2 = dom.append(rows, $('.quick-input-list-row'));
|
||||
|
||||
// Label
|
||||
data.label = new IconLabel(row1, { supportHighlights: true, supportDescriptionHighlights: true, supportCodicons: true });
|
||||
|
||||
// Keybinding
|
||||
const keybindingContainer = dom.append(row1, $('.quick-input-list-entry-keybinding'));
|
||||
data.keybinding = new KeybindingLabel(keybindingContainer, platform.OS);
|
||||
|
||||
// Detail
|
||||
const detailContainer = dom.append(row2, $('.quick-input-list-label-meta'));
|
||||
data.detail = new HighlightedLabel(detailContainer, true);
|
||||
|
||||
// Separator
|
||||
data.separator = dom.append(data.entry, $('.quick-input-list-separator'));
|
||||
|
||||
// Actions
|
||||
data.actionBar = new ActionBar(data.entry);
|
||||
data.actionBar.domNode.classList.add('quick-input-list-entry-action-bar');
|
||||
data.toDisposeTemplate.push(data.actionBar);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
renderElement(element: ListElement, index: number, data: IListElementTemplateData): void {
|
||||
data.toDisposeElement = dispose(data.toDisposeElement);
|
||||
data.element = element;
|
||||
data.checkbox.checked = element.checked;
|
||||
data.toDisposeElement.push(element.onChecked(checked => data.checkbox.checked = checked));
|
||||
|
||||
const { labelHighlights, descriptionHighlights, detailHighlights } = element;
|
||||
|
||||
// Label
|
||||
const options: IIconLabelValueOptions = Object.create(null);
|
||||
options.matches = labelHighlights || [];
|
||||
options.descriptionTitle = element.saneDescription;
|
||||
options.descriptionMatches = descriptionHighlights || [];
|
||||
options.extraClasses = element.item.iconClasses;
|
||||
options.italic = element.item.italic;
|
||||
options.strikethrough = element.item.strikethrough;
|
||||
data.label.setLabel(element.saneLabel, element.saneDescription, options);
|
||||
|
||||
// Keybinding
|
||||
data.keybinding.set(element.item.keybinding);
|
||||
|
||||
// Meta
|
||||
data.detail.set(element.saneDetail, detailHighlights);
|
||||
|
||||
// Separator
|
||||
if (element.separator && element.separator.label) {
|
||||
data.separator.textContent = element.separator.label;
|
||||
data.separator.style.display = '';
|
||||
} else {
|
||||
data.separator.style.display = 'none';
|
||||
}
|
||||
data.entry.classList.toggle('quick-input-list-separator-border', !!element.separator);
|
||||
|
||||
// Actions
|
||||
data.actionBar.clear();
|
||||
const buttons = element.item.buttons;
|
||||
if (buttons && buttons.length) {
|
||||
data.actionBar.push(buttons.map((button, index) => {
|
||||
let cssClasses = button.iconClass || (button.iconPath ? getIconClass(button.iconPath) : undefined);
|
||||
if (button.alwaysVisible) {
|
||||
cssClasses = cssClasses ? `${cssClasses} always-visible` : 'always-visible';
|
||||
}
|
||||
const action = new Action(`id-${index}`, '', cssClasses, true, () => {
|
||||
element.fireButtonTriggered({
|
||||
button,
|
||||
item: element.item
|
||||
});
|
||||
return Promise.resolve();
|
||||
});
|
||||
action.tooltip = button.tooltip || '';
|
||||
return action;
|
||||
}), { icon: true, label: false });
|
||||
data.entry.classList.add('has-actions');
|
||||
} else {
|
||||
data.entry.classList.remove('has-actions');
|
||||
}
|
||||
}
|
||||
|
||||
disposeElement(element: ListElement, index: number, data: IListElementTemplateData): void {
|
||||
data.toDisposeElement = dispose(data.toDisposeElement);
|
||||
}
|
||||
|
||||
disposeTemplate(data: IListElementTemplateData): void {
|
||||
data.toDisposeElement = dispose(data.toDisposeElement);
|
||||
data.toDisposeTemplate = dispose(data.toDisposeTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
class ListElementDelegate implements IListVirtualDelegate<ListElement> {
|
||||
|
||||
getHeight(element: ListElement): number {
|
||||
return element.saneDetail ? 44 : 22;
|
||||
}
|
||||
|
||||
getTemplateId(element: ListElement): string {
|
||||
return ListElementRenderer.ID;
|
||||
}
|
||||
}
|
||||
|
||||
export enum QuickInputListFocus {
|
||||
First = 1,
|
||||
Second,
|
||||
Last,
|
||||
Next,
|
||||
Previous,
|
||||
NextPage,
|
||||
PreviousPage
|
||||
}
|
||||
|
||||
export class QuickInputList {
|
||||
|
||||
readonly id: string;
|
||||
private container: HTMLElement;
|
||||
private list: List<ListElement>;
|
||||
private inputElements: Array<IQuickPickItem | IQuickPickSeparator> = [];
|
||||
private elements: ListElement[] = [];
|
||||
private elementsToIndexes = new Map<IQuickPickItem, number>();
|
||||
matchOnDescription = false;
|
||||
matchOnDetail = false;
|
||||
matchOnLabel = true;
|
||||
sortByLabel = true;
|
||||
private readonly _onChangedAllVisibleChecked = new Emitter<boolean>();
|
||||
onChangedAllVisibleChecked: Event<boolean> = this._onChangedAllVisibleChecked.event;
|
||||
private readonly _onChangedCheckedCount = new Emitter<number>();
|
||||
onChangedCheckedCount: Event<number> = this._onChangedCheckedCount.event;
|
||||
private readonly _onChangedVisibleCount = new Emitter<number>();
|
||||
onChangedVisibleCount: Event<number> = this._onChangedVisibleCount.event;
|
||||
private readonly _onChangedCheckedElements = new Emitter<IQuickPickItem[]>();
|
||||
onChangedCheckedElements: Event<IQuickPickItem[]> = this._onChangedCheckedElements.event;
|
||||
private readonly _onButtonTriggered = new Emitter<IQuickPickItemButtonEvent<IQuickPickItem>>();
|
||||
onButtonTriggered = this._onButtonTriggered.event;
|
||||
private readonly _onKeyDown = new Emitter<StandardKeyboardEvent>();
|
||||
onKeyDown: Event<StandardKeyboardEvent> = this._onKeyDown.event;
|
||||
private readonly _onLeave = new Emitter<void>();
|
||||
onLeave: Event<void> = this._onLeave.event;
|
||||
private _fireCheckedEvents = true;
|
||||
private elementDisposables: IDisposable[] = [];
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
private parent: HTMLElement,
|
||||
id: string,
|
||||
options: IQuickInputOptions,
|
||||
) {
|
||||
this.id = id;
|
||||
this.container = dom.append(this.parent, $('.quick-input-list'));
|
||||
const delegate = new ListElementDelegate();
|
||||
const accessibilityProvider = new QuickInputAccessibilityProvider();
|
||||
this.list = options.createList('QuickInput', this.container, delegate, [new ListElementRenderer()], {
|
||||
identityProvider: { getId: element => element.saneLabel },
|
||||
setRowLineHeight: false,
|
||||
multipleSelectionSupport: false,
|
||||
horizontalScrolling: false,
|
||||
accessibilityProvider
|
||||
} as IListOptions<ListElement>);
|
||||
this.list.getHTMLElement().id = id;
|
||||
this.disposables.push(this.list);
|
||||
this.disposables.push(this.list.onKeyDown(e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
switch (event.keyCode) {
|
||||
case KeyCode.Space:
|
||||
this.toggleCheckbox();
|
||||
break;
|
||||
case KeyCode.KEY_A:
|
||||
if (platform.isMacintosh ? e.metaKey : e.ctrlKey) {
|
||||
this.list.setFocus(range(this.list.length));
|
||||
}
|
||||
break;
|
||||
case KeyCode.UpArrow:
|
||||
const focus1 = this.list.getFocus();
|
||||
if (focus1.length === 1 && focus1[0] === 0) {
|
||||
this._onLeave.fire();
|
||||
}
|
||||
break;
|
||||
case KeyCode.DownArrow:
|
||||
const focus2 = this.list.getFocus();
|
||||
if (focus2.length === 1 && focus2[0] === this.list.length - 1) {
|
||||
this._onLeave.fire();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this._onKeyDown.fire(event);
|
||||
}));
|
||||
this.disposables.push(this.list.onMouseDown(e => {
|
||||
if (e.browserEvent.button !== 2) {
|
||||
// Works around / fixes #64350.
|
||||
e.browserEvent.preventDefault();
|
||||
}
|
||||
}));
|
||||
this.disposables.push(dom.addDisposableListener(this.container, dom.EventType.CLICK, e => {
|
||||
if (e.x || e.y) { // Avoid 'click' triggered by 'space' on checkbox.
|
||||
this._onLeave.fire();
|
||||
}
|
||||
}));
|
||||
this.disposables.push(this.list.onMouseMiddleClick(e => {
|
||||
this._onLeave.fire();
|
||||
}));
|
||||
this.disposables.push(this.list.onContextMenu(e => {
|
||||
if (typeof e.index === 'number') {
|
||||
e.browserEvent.preventDefault();
|
||||
|
||||
// we want to treat a context menu event as
|
||||
// a gesture to open the item at the index
|
||||
// since we do not have any context menu
|
||||
// this enables for example macOS to Ctrl-
|
||||
// click on an item to open it.
|
||||
this.list.setSelection([e.index]);
|
||||
}
|
||||
}));
|
||||
this.disposables.push(
|
||||
this._onChangedAllVisibleChecked,
|
||||
this._onChangedCheckedCount,
|
||||
this._onChangedVisibleCount,
|
||||
this._onChangedCheckedElements,
|
||||
this._onButtonTriggered,
|
||||
this._onLeave,
|
||||
this._onKeyDown
|
||||
);
|
||||
}
|
||||
|
||||
@memoize
|
||||
get onDidChangeFocus() {
|
||||
return Event.map(this.list.onDidChangeFocus, e => e.elements.map(e => e.item));
|
||||
}
|
||||
|
||||
@memoize
|
||||
get onDidChangeSelection() {
|
||||
return Event.map(this.list.onDidChangeSelection, e => ({ items: e.elements.map(e => e.item), event: e.browserEvent }));
|
||||
}
|
||||
|
||||
getAllVisibleChecked() {
|
||||
return this.allVisibleChecked(this.elements, false);
|
||||
}
|
||||
|
||||
private allVisibleChecked(elements: ListElement[], whenNoneVisible = true) {
|
||||
for (let i = 0, n = elements.length; i < n; i++) {
|
||||
const element = elements[i];
|
||||
if (!element.hidden) {
|
||||
if (!element.checked) {
|
||||
return false;
|
||||
} else {
|
||||
whenNoneVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return whenNoneVisible;
|
||||
}
|
||||
|
||||
getCheckedCount() {
|
||||
let count = 0;
|
||||
const elements = this.elements;
|
||||
for (let i = 0, n = elements.length; i < n; i++) {
|
||||
if (elements[i].checked) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
getVisibleCount() {
|
||||
let count = 0;
|
||||
const elements = this.elements;
|
||||
for (let i = 0, n = elements.length; i < n; i++) {
|
||||
if (!elements[i].hidden) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
setAllVisibleChecked(checked: boolean) {
|
||||
try {
|
||||
this._fireCheckedEvents = false;
|
||||
this.elements.forEach(element => {
|
||||
if (!element.hidden) {
|
||||
element.checked = checked;
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
this._fireCheckedEvents = true;
|
||||
this.fireCheckedEvents();
|
||||
}
|
||||
}
|
||||
|
||||
setElements(inputElements: Array<IQuickPickItem | IQuickPickSeparator>): void {
|
||||
this.elementDisposables = dispose(this.elementDisposables);
|
||||
const fireButtonTriggered = (event: IQuickPickItemButtonEvent<IQuickPickItem>) => this.fireButtonTriggered(event);
|
||||
this.inputElements = inputElements;
|
||||
this.elements = inputElements.reduce((result, item, index) => {
|
||||
if (item.type !== 'separator') {
|
||||
const previous = index && inputElements[index - 1];
|
||||
const saneLabel = item.label && item.label.replace(/\r?\n/g, ' ');
|
||||
const saneDescription = item.description && item.description.replace(/\r?\n/g, ' ');
|
||||
const saneDetail = item.detail && item.detail.replace(/\r?\n/g, ' ');
|
||||
const saneAriaLabel = item.ariaLabel || [saneLabel, saneDescription, saneDetail]
|
||||
.map(s => s && parseCodicons(s).text)
|
||||
.filter(s => !!s)
|
||||
.join(', ');
|
||||
|
||||
result.push(new ListElement({
|
||||
index,
|
||||
item,
|
||||
saneLabel,
|
||||
saneAriaLabel,
|
||||
saneDescription,
|
||||
saneDetail,
|
||||
labelHighlights: item.highlights?.label,
|
||||
descriptionHighlights: item.highlights?.description,
|
||||
detailHighlights: item.highlights?.detail,
|
||||
checked: false,
|
||||
separator: previous && previous.type === 'separator' ? previous : undefined,
|
||||
fireButtonTriggered
|
||||
}));
|
||||
}
|
||||
return result;
|
||||
}, [] as ListElement[]);
|
||||
this.elementDisposables.push(...this.elements);
|
||||
this.elementDisposables.push(...this.elements.map(element => element.onChecked(() => this.fireCheckedEvents())));
|
||||
|
||||
this.elementsToIndexes = this.elements.reduce((map, element, index) => {
|
||||
map.set(element.item, index);
|
||||
return map;
|
||||
}, new Map<IQuickPickItem, number>());
|
||||
this.list.splice(0, this.list.length); // Clear focus and selection first, sending the events when the list is empty.
|
||||
this.list.splice(0, this.list.length, this.elements);
|
||||
this._onChangedVisibleCount.fire(this.elements.length);
|
||||
}
|
||||
|
||||
getElementsCount(): number {
|
||||
return this.inputElements.length;
|
||||
}
|
||||
|
||||
getFocusedElements() {
|
||||
return this.list.getFocusedElements()
|
||||
.map(e => e.item);
|
||||
}
|
||||
|
||||
setFocusedElements(items: IQuickPickItem[]) {
|
||||
this.list.setFocus(items
|
||||
.filter(item => this.elementsToIndexes.has(item))
|
||||
.map(item => this.elementsToIndexes.get(item)!));
|
||||
if (items.length > 0) {
|
||||
const focused = this.list.getFocus()[0];
|
||||
if (typeof focused === 'number') {
|
||||
this.list.reveal(focused);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getActiveDescendant() {
|
||||
return this.list.getHTMLElement().getAttribute('aria-activedescendant');
|
||||
}
|
||||
|
||||
getSelectedElements() {
|
||||
return this.list.getSelectedElements()
|
||||
.map(e => e.item);
|
||||
}
|
||||
|
||||
setSelectedElements(items: IQuickPickItem[]) {
|
||||
this.list.setSelection(items
|
||||
.filter(item => this.elementsToIndexes.has(item))
|
||||
.map(item => this.elementsToIndexes.get(item)!));
|
||||
}
|
||||
|
||||
getCheckedElements() {
|
||||
return this.elements.filter(e => e.checked)
|
||||
.map(e => e.item);
|
||||
}
|
||||
|
||||
setCheckedElements(items: IQuickPickItem[]) {
|
||||
try {
|
||||
this._fireCheckedEvents = false;
|
||||
const checked = new Set();
|
||||
for (const item of items) {
|
||||
checked.add(item);
|
||||
}
|
||||
for (const element of this.elements) {
|
||||
element.checked = checked.has(element.item);
|
||||
}
|
||||
} finally {
|
||||
this._fireCheckedEvents = true;
|
||||
this.fireCheckedEvents();
|
||||
}
|
||||
}
|
||||
|
||||
set enabled(value: boolean) {
|
||||
this.list.getHTMLElement().style.pointerEvents = value ? '' : 'none';
|
||||
}
|
||||
|
||||
focus(what: QuickInputListFocus): void {
|
||||
if (!this.list.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (what === QuickInputListFocus.Next && this.list.getFocus()[0] === this.list.length - 1) {
|
||||
what = QuickInputListFocus.First;
|
||||
}
|
||||
|
||||
if (what === QuickInputListFocus.Previous && this.list.getFocus()[0] === 0) {
|
||||
what = QuickInputListFocus.Last;
|
||||
}
|
||||
|
||||
if (what === QuickInputListFocus.Second && this.list.length < 2) {
|
||||
what = QuickInputListFocus.First;
|
||||
}
|
||||
|
||||
switch (what) {
|
||||
case QuickInputListFocus.First:
|
||||
this.list.focusFirst();
|
||||
break;
|
||||
case QuickInputListFocus.Second:
|
||||
this.list.focusNth(1);
|
||||
break;
|
||||
case QuickInputListFocus.Last:
|
||||
this.list.focusLast();
|
||||
break;
|
||||
case QuickInputListFocus.Next:
|
||||
this.list.focusNext();
|
||||
break;
|
||||
case QuickInputListFocus.Previous:
|
||||
this.list.focusPrevious();
|
||||
break;
|
||||
case QuickInputListFocus.NextPage:
|
||||
this.list.focusNextPage();
|
||||
break;
|
||||
case QuickInputListFocus.PreviousPage:
|
||||
this.list.focusPreviousPage();
|
||||
break;
|
||||
}
|
||||
|
||||
const focused = this.list.getFocus()[0];
|
||||
if (typeof focused === 'number') {
|
||||
this.list.reveal(focused);
|
||||
}
|
||||
}
|
||||
|
||||
clearFocus() {
|
||||
this.list.setFocus([]);
|
||||
}
|
||||
|
||||
domFocus() {
|
||||
this.list.domFocus();
|
||||
}
|
||||
|
||||
layout(maxHeight?: number): void {
|
||||
this.list.getHTMLElement().style.maxHeight = maxHeight ? `calc(${Math.floor(maxHeight / 44) * 44}px)` : '';
|
||||
this.list.layout();
|
||||
}
|
||||
|
||||
filter(query: string): boolean {
|
||||
if (!(this.sortByLabel || this.matchOnLabel || this.matchOnDescription || this.matchOnDetail)) {
|
||||
this.list.layout();
|
||||
return false;
|
||||
}
|
||||
query = query.trim();
|
||||
|
||||
// Reset filtering
|
||||
if (!query || !(this.matchOnLabel || this.matchOnDescription || this.matchOnDetail)) {
|
||||
this.elements.forEach(element => {
|
||||
element.labelHighlights = undefined;
|
||||
element.descriptionHighlights = undefined;
|
||||
element.detailHighlights = undefined;
|
||||
element.hidden = false;
|
||||
const previous = element.index && this.inputElements[element.index - 1];
|
||||
element.separator = previous && previous.type === 'separator' ? previous : undefined;
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by value (since we support codicons, use codicon aware fuzzy matching)
|
||||
else {
|
||||
this.elements.forEach(element => {
|
||||
const labelHighlights = this.matchOnLabel ? withNullAsUndefined(matchesFuzzyCodiconAware(query, parseCodicons(element.saneLabel))) : undefined;
|
||||
const descriptionHighlights = this.matchOnDescription ? withNullAsUndefined(matchesFuzzyCodiconAware(query, parseCodicons(element.saneDescription || ''))) : undefined;
|
||||
const detailHighlights = this.matchOnDetail ? withNullAsUndefined(matchesFuzzyCodiconAware(query, parseCodicons(element.saneDetail || ''))) : undefined;
|
||||
|
||||
if (labelHighlights || descriptionHighlights || detailHighlights) {
|
||||
element.labelHighlights = labelHighlights;
|
||||
element.descriptionHighlights = descriptionHighlights;
|
||||
element.detailHighlights = detailHighlights;
|
||||
element.hidden = false;
|
||||
} else {
|
||||
element.labelHighlights = undefined;
|
||||
element.descriptionHighlights = undefined;
|
||||
element.detailHighlights = undefined;
|
||||
element.hidden = !element.item.alwaysShow;
|
||||
}
|
||||
element.separator = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
const shownElements = this.elements.filter(element => !element.hidden);
|
||||
|
||||
// Sort by value
|
||||
if (this.sortByLabel && query) {
|
||||
const normalizedSearchValue = query.toLowerCase();
|
||||
shownElements.sort((a, b) => {
|
||||
return compareEntries(a, b, normalizedSearchValue);
|
||||
});
|
||||
}
|
||||
|
||||
this.elementsToIndexes = shownElements.reduce((map, element, index) => {
|
||||
map.set(element.item, index);
|
||||
return map;
|
||||
}, new Map<IQuickPickItem, number>());
|
||||
this.list.splice(0, this.list.length, shownElements);
|
||||
this.list.setFocus([]);
|
||||
this.list.layout();
|
||||
|
||||
this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked());
|
||||
this._onChangedVisibleCount.fire(shownElements.length);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
toggleCheckbox() {
|
||||
try {
|
||||
this._fireCheckedEvents = false;
|
||||
const elements = this.list.getFocusedElements();
|
||||
const allChecked = this.allVisibleChecked(elements);
|
||||
for (const element of elements) {
|
||||
element.checked = !allChecked;
|
||||
}
|
||||
} finally {
|
||||
this._fireCheckedEvents = true;
|
||||
this.fireCheckedEvents();
|
||||
}
|
||||
}
|
||||
|
||||
display(display: boolean) {
|
||||
this.container.style.display = display ? '' : 'none';
|
||||
}
|
||||
|
||||
isDisplayed() {
|
||||
return this.container.style.display !== 'none';
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.elementDisposables = dispose(this.elementDisposables);
|
||||
this.disposables = dispose(this.disposables);
|
||||
}
|
||||
|
||||
private fireCheckedEvents() {
|
||||
if (this._fireCheckedEvents) {
|
||||
this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked());
|
||||
this._onChangedCheckedCount.fire(this.getCheckedCount());
|
||||
this._onChangedCheckedElements.fire(this.getCheckedElements());
|
||||
}
|
||||
}
|
||||
|
||||
private fireButtonTriggered(event: IQuickPickItemButtonEvent<IQuickPickItem>) {
|
||||
this._onButtonTriggered.fire(event);
|
||||
}
|
||||
|
||||
style(styles: IListStyles) {
|
||||
this.list.style(styles);
|
||||
}
|
||||
}
|
||||
|
||||
function compareEntries(elementA: ListElement, elementB: ListElement, lookFor: string): number {
|
||||
|
||||
const labelHighlightsA = elementA.labelHighlights || [];
|
||||
const labelHighlightsB = elementB.labelHighlights || [];
|
||||
if (labelHighlightsA.length && !labelHighlightsB.length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!labelHighlightsA.length && labelHighlightsB.length) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (labelHighlightsA.length === 0 && labelHighlightsB.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return compareAnything(elementA.saneLabel, elementB.saneLabel, lookFor);
|
||||
}
|
||||
|
||||
class QuickInputAccessibilityProvider implements IListAccessibilityProvider<ListElement> {
|
||||
|
||||
getWidgetAriaLabel(): string {
|
||||
return localize('quickInput', "Quick Input");
|
||||
}
|
||||
|
||||
getAriaLabel(element: ListElement): string | null {
|
||||
return element.saneAriaLabel;
|
||||
}
|
||||
|
||||
getWidgetRole() {
|
||||
return 'listbox';
|
||||
}
|
||||
|
||||
getRole() {
|
||||
return 'option';
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/quickInput';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IdGenerator } from 'vs/base/common/idGenerator';
|
||||
|
||||
const iconPathToClass: Record<string, string> = {};
|
||||
const iconClassGenerator = new IdGenerator('quick-input-button-icon-');
|
||||
|
||||
export function getIconClass(iconPath: { dark: URI; light?: URI; } | undefined): string | undefined {
|
||||
if (!iconPath) {
|
||||
return undefined;
|
||||
}
|
||||
let iconClass: string;
|
||||
|
||||
const key = iconPath.dark.toString();
|
||||
if (iconPathToClass[key]) {
|
||||
iconClass = iconPathToClass[key];
|
||||
} else {
|
||||
iconClass = iconClassGenerator.nextId();
|
||||
dom.createCSSRule(`.${iconClass}`, `background-image: ${dom.asCSSUrl(iconPath.light || iconPath.dark)}`);
|
||||
dom.createCSSRule(`.vs-dark .${iconClass}, .hc-black .${iconClass}`, `background-image: ${dom.asCSSUrl(iconPath.dark)}`);
|
||||
iconPathToClass[key] = iconClass;
|
||||
}
|
||||
|
||||
return iconClass;
|
||||
}
|
365
lib/vscode/src/vs/base/parts/quickinput/common/quickInput.ts
Normal file
365
lib/vscode/src/vs/base/parts/quickinput/common/quickInput.ts
Normal file
@ -0,0 +1,365 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IMatch } from 'vs/base/common/filters';
|
||||
import { IItemAccessor } from 'vs/base/common/fuzzyScorer';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
|
||||
export interface IQuickPickItemHighlights {
|
||||
label?: IMatch[];
|
||||
description?: IMatch[];
|
||||
detail?: IMatch[];
|
||||
}
|
||||
|
||||
export interface IQuickPickItem {
|
||||
type?: 'item';
|
||||
id?: string;
|
||||
label: string;
|
||||
ariaLabel?: string;
|
||||
description?: string;
|
||||
detail?: string;
|
||||
/**
|
||||
* Allows to show a keybinding next to the item to indicate
|
||||
* how the item can be triggered outside of the picker using
|
||||
* keyboard shortcut.
|
||||
*/
|
||||
keybinding?: ResolvedKeybinding;
|
||||
iconClasses?: string[];
|
||||
italic?: boolean;
|
||||
strikethrough?: boolean;
|
||||
highlights?: IQuickPickItemHighlights;
|
||||
buttons?: IQuickInputButton[];
|
||||
picked?: boolean;
|
||||
alwaysShow?: boolean;
|
||||
}
|
||||
|
||||
export interface IQuickPickSeparator {
|
||||
type: 'separator';
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface IKeyMods {
|
||||
readonly ctrlCmd: boolean;
|
||||
readonly alt: boolean;
|
||||
}
|
||||
|
||||
export const NO_KEY_MODS: IKeyMods = { ctrlCmd: false, alt: false };
|
||||
|
||||
export interface IQuickNavigateConfiguration {
|
||||
keybindings: ResolvedKeybinding[];
|
||||
}
|
||||
|
||||
export interface IPickOptions<T extends IQuickPickItem> {
|
||||
|
||||
/**
|
||||
* an optional string to show as placeholder in the input box to guide the user what she picks on
|
||||
*/
|
||||
placeHolder?: string;
|
||||
|
||||
/**
|
||||
* an optional flag to include the description when filtering the picks
|
||||
*/
|
||||
matchOnDescription?: boolean;
|
||||
|
||||
/**
|
||||
* an optional flag to include the detail when filtering the picks
|
||||
*/
|
||||
matchOnDetail?: boolean;
|
||||
|
||||
/**
|
||||
* an optional flag to filter the picks based on label. Defaults to true.
|
||||
*/
|
||||
matchOnLabel?: boolean;
|
||||
|
||||
/**
|
||||
* an option flag to control whether focus is always automatically brought to a list item. Defaults to true.
|
||||
*/
|
||||
autoFocusOnList?: boolean;
|
||||
|
||||
/**
|
||||
* an optional flag to not close the picker on focus lost
|
||||
*/
|
||||
ignoreFocusLost?: boolean;
|
||||
|
||||
/**
|
||||
* an optional flag to make this picker multi-select
|
||||
*/
|
||||
canPickMany?: boolean;
|
||||
|
||||
/**
|
||||
* enables quick navigate in the picker to open an element without typing
|
||||
*/
|
||||
quickNavigate?: IQuickNavigateConfiguration;
|
||||
|
||||
/**
|
||||
* a context key to set when this picker is active
|
||||
*/
|
||||
contextKey?: string;
|
||||
|
||||
/**
|
||||
* an optional property for the item to focus initially.
|
||||
*/
|
||||
activeItem?: Promise<T> | T;
|
||||
|
||||
onKeyMods?: (keyMods: IKeyMods) => void;
|
||||
onDidFocus?: (entry: T) => void;
|
||||
onDidTriggerItemButton?: (context: IQuickPickItemButtonContext<T>) => void;
|
||||
}
|
||||
|
||||
export interface IInputOptions {
|
||||
|
||||
/**
|
||||
* the value to prefill in the input box
|
||||
*/
|
||||
value?: string;
|
||||
|
||||
/**
|
||||
* the selection of value, default to the whole word
|
||||
*/
|
||||
valueSelection?: [number, number];
|
||||
|
||||
/**
|
||||
* the text to display underneath the input box
|
||||
*/
|
||||
prompt?: string;
|
||||
|
||||
/**
|
||||
* an optional string to show as placeholder in the input box to guide the user what to type
|
||||
*/
|
||||
placeHolder?: string;
|
||||
|
||||
/**
|
||||
* Controls if a password input is shown. Password input hides the typed text.
|
||||
*/
|
||||
password?: boolean;
|
||||
|
||||
ignoreFocusLost?: boolean;
|
||||
|
||||
/**
|
||||
* an optional function that is used to validate user input.
|
||||
*/
|
||||
validateInput?: (input: string) => Promise<string | null | undefined>;
|
||||
}
|
||||
|
||||
export interface IQuickInput extends IDisposable {
|
||||
|
||||
readonly onDidHide: Event<void>;
|
||||
readonly onDispose: Event<void>;
|
||||
|
||||
title: string | undefined;
|
||||
|
||||
description: string | undefined;
|
||||
|
||||
step: number | undefined;
|
||||
|
||||
totalSteps: number | undefined;
|
||||
|
||||
enabled: boolean;
|
||||
|
||||
contextKey: string | undefined;
|
||||
|
||||
busy: boolean;
|
||||
|
||||
ignoreFocusOut: boolean;
|
||||
|
||||
show(): void;
|
||||
|
||||
hide(): void;
|
||||
}
|
||||
|
||||
export interface IQuickPickAcceptEvent {
|
||||
|
||||
/**
|
||||
* Signals if the picker item is to be accepted
|
||||
* in the background while keeping the picker open.
|
||||
*/
|
||||
inBackground: boolean;
|
||||
}
|
||||
|
||||
export enum ItemActivation {
|
||||
NONE,
|
||||
FIRST,
|
||||
SECOND,
|
||||
LAST
|
||||
}
|
||||
|
||||
export interface IQuickPick<T extends IQuickPickItem> extends IQuickInput {
|
||||
|
||||
value: string;
|
||||
|
||||
/**
|
||||
* A method that allows to massage the value used
|
||||
* for filtering, e.g, to remove certain parts.
|
||||
*/
|
||||
filterValue: (value: string) => string;
|
||||
|
||||
ariaLabel: string | undefined;
|
||||
|
||||
placeholder: string | undefined;
|
||||
|
||||
readonly onDidChangeValue: Event<string>;
|
||||
|
||||
readonly onDidAccept: Event<IQuickPickAcceptEvent>;
|
||||
|
||||
/**
|
||||
* If enabled, will fire the `onDidAccept` event when
|
||||
* pressing the arrow-right key with the idea of accepting
|
||||
* the selected item without closing the picker.
|
||||
*/
|
||||
canAcceptInBackground: boolean;
|
||||
|
||||
ok: boolean | 'default';
|
||||
|
||||
readonly onDidCustom: Event<void>;
|
||||
|
||||
customButton: boolean;
|
||||
|
||||
customLabel: string | undefined;
|
||||
|
||||
customHover: string | undefined;
|
||||
|
||||
buttons: ReadonlyArray<IQuickInputButton>;
|
||||
|
||||
readonly onDidTriggerButton: Event<IQuickInputButton>;
|
||||
|
||||
readonly onDidTriggerItemButton: Event<IQuickPickItemButtonEvent<T>>;
|
||||
|
||||
items: ReadonlyArray<T | IQuickPickSeparator>;
|
||||
|
||||
canSelectMany: boolean;
|
||||
|
||||
matchOnDescription: boolean;
|
||||
|
||||
matchOnDetail: boolean;
|
||||
|
||||
matchOnLabel: boolean;
|
||||
|
||||
sortByLabel: boolean;
|
||||
|
||||
autoFocusOnList: boolean;
|
||||
|
||||
quickNavigate: IQuickNavigateConfiguration | undefined;
|
||||
|
||||
activeItems: ReadonlyArray<T>;
|
||||
|
||||
readonly onDidChangeActive: Event<T[]>;
|
||||
|
||||
/**
|
||||
* Allows to control which entry should be activated by default.
|
||||
*/
|
||||
itemActivation: ItemActivation;
|
||||
|
||||
selectedItems: ReadonlyArray<T>;
|
||||
|
||||
readonly onDidChangeSelection: Event<T[]>;
|
||||
|
||||
readonly keyMods: IKeyMods;
|
||||
|
||||
valueSelection: Readonly<[number, number]> | undefined;
|
||||
|
||||
validationMessage: string | undefined;
|
||||
|
||||
inputHasFocus(): boolean;
|
||||
|
||||
focusOnInput(): void;
|
||||
|
||||
/**
|
||||
* Hides the input box from the picker UI. This is typically used
|
||||
* in combination with quick-navigation where no search UI should
|
||||
* be presented.
|
||||
*/
|
||||
hideInput: boolean;
|
||||
|
||||
hideCheckAll: boolean;
|
||||
}
|
||||
|
||||
export interface IInputBox extends IQuickInput {
|
||||
|
||||
value: string;
|
||||
|
||||
valueSelection: Readonly<[number, number]> | undefined;
|
||||
|
||||
placeholder: string | undefined;
|
||||
|
||||
password: boolean;
|
||||
|
||||
readonly onDidChangeValue: Event<string>;
|
||||
|
||||
readonly onDidAccept: Event<void>;
|
||||
|
||||
buttons: ReadonlyArray<IQuickInputButton>;
|
||||
|
||||
readonly onDidTriggerButton: Event<IQuickInputButton>;
|
||||
|
||||
prompt: string | undefined;
|
||||
|
||||
validationMessage: string | undefined;
|
||||
}
|
||||
|
||||
export interface IQuickInputButton {
|
||||
/** iconPath or iconClass required */
|
||||
iconPath?: { dark: URI; light?: URI; };
|
||||
/** iconPath or iconClass required */
|
||||
iconClass?: string;
|
||||
tooltip?: string;
|
||||
/**
|
||||
* Whether to always show the button. By default buttons
|
||||
* are only visible when hovering over them with the mouse
|
||||
*/
|
||||
alwaysVisible?: boolean;
|
||||
}
|
||||
|
||||
export interface IQuickPickItemButtonEvent<T extends IQuickPickItem> {
|
||||
button: IQuickInputButton;
|
||||
item: T;
|
||||
}
|
||||
|
||||
export interface IQuickPickItemButtonContext<T extends IQuickPickItem> extends IQuickPickItemButtonEvent<T> {
|
||||
removeItem(): void;
|
||||
}
|
||||
|
||||
export type QuickPickInput<T = IQuickPickItem> = T | IQuickPickSeparator;
|
||||
|
||||
|
||||
//region Fuzzy Scorer Support
|
||||
|
||||
export type IQuickPickItemWithResource = IQuickPickItem & { resource?: URI };
|
||||
|
||||
export class QuickPickItemScorerAccessor implements IItemAccessor<IQuickPickItemWithResource> {
|
||||
|
||||
constructor(private options?: { skipDescription?: boolean, skipPath?: boolean }) { }
|
||||
|
||||
getItemLabel(entry: IQuickPickItemWithResource): string {
|
||||
return entry.label;
|
||||
}
|
||||
|
||||
getItemDescription(entry: IQuickPickItemWithResource): string | undefined {
|
||||
if (this.options?.skipDescription) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return entry.description;
|
||||
}
|
||||
|
||||
getItemPath(entry: IQuickPickItemWithResource): string | undefined {
|
||||
if (this.options?.skipPath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (entry.resource?.scheme === Schemas.file) {
|
||||
return entry.resource.fsPath;
|
||||
}
|
||||
|
||||
return entry.resource?.path;
|
||||
}
|
||||
}
|
||||
|
||||
export const quickPickItemScorerAccessor = new QuickPickItemScorerAccessor();
|
||||
|
||||
//#endregion
|
76
lib/vscode/src/vs/base/parts/request/browser/request.ts
Normal file
76
lib/vscode/src/vs/base/parts/request/browser/request.ts
Normal file
@ -0,0 +1,76 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { canceled } from 'vs/base/common/errors';
|
||||
import { VSBuffer, bufferToStream } from 'vs/base/common/buffer';
|
||||
import { IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request';
|
||||
|
||||
export function request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
|
||||
if (options.proxyAuthorization) {
|
||||
options.headers = {
|
||||
...(options.headers || {}),
|
||||
'Proxy-Authorization': options.proxyAuthorization
|
||||
};
|
||||
}
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
return new Promise<IRequestContext>((resolve, reject) => {
|
||||
|
||||
xhr.open(options.type || 'GET', options.url || '', true, options.user, options.password);
|
||||
setRequestHeaders(xhr, options);
|
||||
|
||||
xhr.responseType = 'arraybuffer';
|
||||
xhr.onerror = e => reject(new Error(xhr.statusText && ('XHR failed: ' + xhr.statusText) || 'XHR failed'));
|
||||
xhr.onload = (e) => {
|
||||
resolve({
|
||||
res: {
|
||||
statusCode: xhr.status,
|
||||
headers: getResponseHeaders(xhr)
|
||||
},
|
||||
stream: bufferToStream(VSBuffer.wrap(new Uint8Array(xhr.response)))
|
||||
});
|
||||
};
|
||||
xhr.ontimeout = e => reject(new Error(`XHR timeout: ${options.timeout}ms`));
|
||||
|
||||
if (options.timeout) {
|
||||
xhr.timeout = options.timeout;
|
||||
}
|
||||
|
||||
xhr.send(options.data);
|
||||
|
||||
// cancel
|
||||
token.onCancellationRequested(() => {
|
||||
xhr.abort();
|
||||
reject(canceled());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setRequestHeaders(xhr: XMLHttpRequest, options: IRequestOptions): void {
|
||||
if (options.headers) {
|
||||
outer: for (let k in options.headers) {
|
||||
switch (k) {
|
||||
case 'User-Agent':
|
||||
case 'Accept-Encoding':
|
||||
case 'Content-Length':
|
||||
// unsafe headers
|
||||
continue outer;
|
||||
}
|
||||
xhr.setRequestHeader(k, options.headers[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getResponseHeaders(xhr: XMLHttpRequest): { [name: string]: string } {
|
||||
const headers: { [name: string]: string } = Object.create(null);
|
||||
for (const line of xhr.getAllResponseHeaders().split(/\r\n|\n|\r/g)) {
|
||||
if (line) {
|
||||
const idx = line.indexOf(':');
|
||||
headers[line.substr(0, idx).trim().toLowerCase()] = line.substr(idx + 1).trim();
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
30
lib/vscode/src/vs/base/parts/request/common/request.ts
Normal file
30
lib/vscode/src/vs/base/parts/request/common/request.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { VSBufferReadableStream } from 'vs/base/common/buffer';
|
||||
|
||||
export interface IHeaders {
|
||||
[header: string]: string;
|
||||
}
|
||||
|
||||
export interface IRequestOptions {
|
||||
type?: string;
|
||||
url?: string;
|
||||
user?: string;
|
||||
password?: string;
|
||||
headers?: IHeaders;
|
||||
timeout?: number;
|
||||
data?: string;
|
||||
followRedirects?: number;
|
||||
proxyAuthorization?: string;
|
||||
}
|
||||
|
||||
export interface IRequestContext {
|
||||
res: {
|
||||
headers: IHeaders;
|
||||
statusCode?: number;
|
||||
};
|
||||
stream: VSBufferReadableStream;
|
||||
}
|
253
lib/vscode/src/vs/base/parts/sandbox/common/electronTypes.ts
Normal file
253
lib/vscode/src/vs/base/parts/sandbox/common/electronTypes.ts
Normal file
@ -0,0 +1,253 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
// #######################################################################
|
||||
// ### ###
|
||||
// ### electron.d.ts types we need in a common layer for reuse ###
|
||||
// ### (copied from Electron 9.x) ###
|
||||
// ### ###
|
||||
// #######################################################################
|
||||
|
||||
|
||||
export interface MessageBoxOptions {
|
||||
/**
|
||||
* Can be `"none"`, `"info"`, `"error"`, `"question"` or `"warning"`. On Windows,
|
||||
* `"question"` displays the same icon as `"info"`, unless you set an icon using
|
||||
* the `"icon"` option. On macOS, both `"warning"` and `"error"` display the same
|
||||
* warning icon.
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* Array of texts for buttons. On Windows, an empty array will result in one button
|
||||
* labeled "OK".
|
||||
*/
|
||||
buttons?: string[];
|
||||
/**
|
||||
* Index of the button in the buttons array which will be selected by default when
|
||||
* the message box opens.
|
||||
*/
|
||||
defaultId?: number;
|
||||
/**
|
||||
* Title of the message box, some platforms will not show it.
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* Content of the message box.
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Extra information of the message.
|
||||
*/
|
||||
detail?: string;
|
||||
/**
|
||||
* If provided, the message box will include a checkbox with the given label.
|
||||
*/
|
||||
checkboxLabel?: string;
|
||||
/**
|
||||
* Initial checked state of the checkbox. `false` by default.
|
||||
*/
|
||||
checkboxChecked?: boolean;
|
||||
// icon?: NativeImage;
|
||||
/**
|
||||
* The index of the button to be used to cancel the dialog, via the `Esc` key. By
|
||||
* default this is assigned to the first button with "cancel" or "no" as the label.
|
||||
* If no such labeled buttons exist and this option is not set, `0` will be used as
|
||||
* the return value.
|
||||
*/
|
||||
cancelId?: number;
|
||||
/**
|
||||
* On Windows Electron will try to figure out which one of the `buttons` are common
|
||||
* buttons (like "Cancel" or "Yes"), and show the others as command links in the
|
||||
* dialog. This can make the dialog appear in the style of modern Windows apps. If
|
||||
* you don't like this behavior, you can set `noLink` to `true`.
|
||||
*/
|
||||
noLink?: boolean;
|
||||
/**
|
||||
* Normalize the keyboard access keys across platforms. Default is `false`.
|
||||
* Enabling this assumes `&` is used in the button labels for the placement of the
|
||||
* keyboard shortcut access key and labels will be converted so they work correctly
|
||||
* on each platform, `&` characters are removed on macOS, converted to `_` on
|
||||
* Linux, and left untouched on Windows. For example, a button label of `Vie&w`
|
||||
* will be converted to `Vie_w` on Linux and `View` on macOS and can be selected
|
||||
* via `Alt-W` on Windows and Linux.
|
||||
*/
|
||||
normalizeAccessKeys?: boolean;
|
||||
}
|
||||
|
||||
export interface MessageBoxReturnValue {
|
||||
/**
|
||||
* The index of the clicked button.
|
||||
*/
|
||||
response: number;
|
||||
/**
|
||||
* The checked state of the checkbox if `checkboxLabel` was set. Otherwise `false`.
|
||||
*/
|
||||
checkboxChecked: boolean;
|
||||
}
|
||||
|
||||
export interface OpenDevToolsOptions {
|
||||
/**
|
||||
* Opens the devtools with specified dock state, can be `right`, `bottom`,
|
||||
* `undocked`, `detach`. Defaults to last used dock state. In `undocked` mode it's
|
||||
* possible to dock back. In `detach` mode it's not.
|
||||
*/
|
||||
mode: ('right' | 'bottom' | 'undocked' | 'detach');
|
||||
/**
|
||||
* Whether to bring the opened devtools window to the foreground. The default is
|
||||
* `true`.
|
||||
*/
|
||||
activate?: boolean;
|
||||
}
|
||||
|
||||
export interface SaveDialogOptions {
|
||||
title?: string;
|
||||
/**
|
||||
* Absolute directory path, absolute file path, or file name to use by default.
|
||||
*/
|
||||
defaultPath?: string;
|
||||
/**
|
||||
* Custom label for the confirmation button, when left empty the default label will
|
||||
* be used.
|
||||
*/
|
||||
buttonLabel?: string;
|
||||
filters?: FileFilter[];
|
||||
/**
|
||||
* Message to display above text fields.
|
||||
*
|
||||
* @platform darwin
|
||||
*/
|
||||
message?: string;
|
||||
/**
|
||||
* Custom label for the text displayed in front of the filename text field.
|
||||
*
|
||||
* @platform darwin
|
||||
*/
|
||||
nameFieldLabel?: string;
|
||||
/**
|
||||
* Show the tags input box, defaults to `true`.
|
||||
*
|
||||
* @platform darwin
|
||||
*/
|
||||
showsTagField?: boolean;
|
||||
properties?: Array<'showHiddenFiles' | 'createDirectory' | 'treatPackageAsDirectory' | 'showOverwriteConfirmation' | 'dontAddToRecent'>;
|
||||
/**
|
||||
* Create a security scoped bookmark when packaged for the Mac App Store. If this
|
||||
* option is enabled and the file doesn't already exist a blank file will be
|
||||
* created at the chosen path.
|
||||
*
|
||||
* @platform darwin,mas
|
||||
*/
|
||||
securityScopedBookmarks?: boolean;
|
||||
}
|
||||
|
||||
export interface OpenDialogOptions {
|
||||
title?: string;
|
||||
defaultPath?: string;
|
||||
/**
|
||||
* Custom label for the confirmation button, when left empty the default label will
|
||||
* be used.
|
||||
*/
|
||||
buttonLabel?: string;
|
||||
filters?: FileFilter[];
|
||||
/**
|
||||
* Contains which features the dialog should use. The following values are
|
||||
* supported:
|
||||
*/
|
||||
properties?: Array<'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles' | 'createDirectory' | 'promptToCreate' | 'noResolveAliases' | 'treatPackageAsDirectory' | 'dontAddToRecent'>;
|
||||
/**
|
||||
* Message to display above input boxes.
|
||||
*
|
||||
* @platform darwin
|
||||
*/
|
||||
message?: string;
|
||||
/**
|
||||
* Create security scoped bookmarks when packaged for the Mac App Store.
|
||||
*
|
||||
* @platform darwin,mas
|
||||
*/
|
||||
securityScopedBookmarks?: boolean;
|
||||
}
|
||||
|
||||
export interface OpenDialogReturnValue {
|
||||
/**
|
||||
* whether or not the dialog was canceled.
|
||||
*/
|
||||
canceled: boolean;
|
||||
/**
|
||||
* An array of file paths chosen by the user. If the dialog is cancelled this will
|
||||
* be an empty array.
|
||||
*/
|
||||
filePaths: string[];
|
||||
/**
|
||||
* An array matching the `filePaths` array of base64 encoded strings which contains
|
||||
* security scoped bookmark data. `securityScopedBookmarks` must be enabled for
|
||||
* this to be populated. (For return values, see table here.)
|
||||
*
|
||||
* @platform darwin,mas
|
||||
*/
|
||||
bookmarks?: string[];
|
||||
}
|
||||
|
||||
export interface SaveDialogReturnValue {
|
||||
/**
|
||||
* whether or not the dialog was canceled.
|
||||
*/
|
||||
canceled: boolean;
|
||||
/**
|
||||
* If the dialog is canceled, this will be `undefined`.
|
||||
*/
|
||||
filePath?: string;
|
||||
/**
|
||||
* Base64 encoded string which contains the security scoped bookmark data for the
|
||||
* saved file. `securityScopedBookmarks` must be enabled for this to be present.
|
||||
* (For return values, see table here.)
|
||||
*
|
||||
* @platform darwin,mas
|
||||
*/
|
||||
bookmark?: string;
|
||||
}
|
||||
|
||||
export interface FileFilter {
|
||||
|
||||
// Docs: http://electronjs.org/docs/api/structures/file-filter
|
||||
|
||||
extensions: string[];
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface InputEvent {
|
||||
|
||||
// Docs: http://electronjs.org/docs/api/structures/input-event
|
||||
|
||||
/**
|
||||
* An array of modifiers of the event, can be `shift`, `control`, `ctrl`, `alt`,
|
||||
* `meta`, `command`, `cmd`, `isKeypad`, `isAutoRepeat`, `leftButtonDown`,
|
||||
* `middleButtonDown`, `rightButtonDown`, `capsLock`, `numLock`, `left`, `right`.
|
||||
*/
|
||||
modifiers?: Array<'shift' | 'control' | 'ctrl' | 'alt' | 'meta' | 'command' | 'cmd' | 'isKeypad' | 'isAutoRepeat' | 'leftButtonDown' | 'middleButtonDown' | 'rightButtonDown' | 'capsLock' | 'numLock' | 'left' | 'right'>;
|
||||
}
|
||||
|
||||
export interface MouseInputEvent extends InputEvent {
|
||||
|
||||
// Docs: http://electronjs.org/docs/api/structures/mouse-input-event
|
||||
|
||||
/**
|
||||
* The button pressed, can be `left`, `middle`, `right`.
|
||||
*/
|
||||
button?: ('left' | 'middle' | 'right');
|
||||
clickCount?: number;
|
||||
globalX?: number;
|
||||
globalY?: number;
|
||||
movementX?: number;
|
||||
movementY?: number;
|
||||
/**
|
||||
* The type of the event, can be `mouseDown`, `mouseUp`, `mouseEnter`,
|
||||
* `mouseLeave`, `contextMenu`, `mouseWheel` or `mouseMove`.
|
||||
*/
|
||||
type: ('mouseDown' | 'mouseUp' | 'mouseEnter' | 'mouseLeave' | 'contextMenu' | 'mouseWheel' | 'mouseMove');
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
250
lib/vscode/src/vs/base/parts/sandbox/electron-browser/preload.js
Normal file
250
lib/vscode/src/vs/base/parts/sandbox/electron-browser/preload.js
Normal file
@ -0,0 +1,250 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// @ts-check
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const { ipcRenderer, webFrame, crashReporter, contextBridge } = require('electron');
|
||||
|
||||
// #######################################################################
|
||||
// ### ###
|
||||
// ### !!! DO NOT USE GET/SET PROPERTIES ANYWHERE HERE !!! ###
|
||||
// ### !!! UNLESS THE ACCESS IS WITHOUT SIDE EFFECTS !!! ###
|
||||
// ### (https://github.com/electron/electron/issues/25516) ###
|
||||
// ### ###
|
||||
// #######################################################################
|
||||
|
||||
const globals = {
|
||||
|
||||
/**
|
||||
* A minimal set of methods exposed from Electron's `ipcRenderer`
|
||||
* to support communication to main process.
|
||||
*/
|
||||
ipcRenderer: {
|
||||
|
||||
/**
|
||||
* @param {string} channel
|
||||
* @param {any[]} args
|
||||
*/
|
||||
send(channel, ...args) {
|
||||
if (validateIPC(channel)) {
|
||||
ipcRenderer.send(channel, ...args);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} channel
|
||||
* @param {(event: import('electron').IpcRendererEvent, ...args: any[]) => void} listener
|
||||
*/
|
||||
on(channel, listener) {
|
||||
if (validateIPC(channel)) {
|
||||
ipcRenderer.on(channel, listener);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} channel
|
||||
* @param {(event: import('electron').IpcRendererEvent, ...args: any[]) => void} listener
|
||||
*/
|
||||
once(channel, listener) {
|
||||
if (validateIPC(channel)) {
|
||||
ipcRenderer.once(channel, listener);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} channel
|
||||
* @param {(event: import('electron').IpcRendererEvent, ...args: any[]) => void} listener
|
||||
*/
|
||||
removeListener(channel, listener) {
|
||||
if (validateIPC(channel)) {
|
||||
ipcRenderer.removeListener(channel, listener);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Support for subset of methods of Electron's `webFrame` type.
|
||||
*/
|
||||
webFrame: {
|
||||
|
||||
/**
|
||||
* @param {number} level
|
||||
*/
|
||||
setZoomLevel(level) {
|
||||
if (typeof level === 'number') {
|
||||
webFrame.setZoomLevel(level);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Support for subset of methods of Electron's `crashReporter` type.
|
||||
*/
|
||||
crashReporter: {
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {string} value
|
||||
*/
|
||||
addExtraParameter(key, value) {
|
||||
crashReporter.addExtraParameter(key, value);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Support for a subset of access to node.js global `process`.
|
||||
*/
|
||||
process: {
|
||||
get platform() { return process.platform; },
|
||||
get env() { return process.env; },
|
||||
get versions() { return process.versions; },
|
||||
get type() { return 'renderer'; },
|
||||
|
||||
_whenEnvResolved: undefined,
|
||||
whenEnvResolved:
|
||||
/**
|
||||
* @returns when the shell environment has been resolved.
|
||||
*/
|
||||
function () {
|
||||
if (!this._whenEnvResolved) {
|
||||
this._whenEnvResolved = resolveEnv();
|
||||
}
|
||||
|
||||
return this._whenEnvResolved;
|
||||
},
|
||||
|
||||
nextTick:
|
||||
/**
|
||||
* Adds callback to the "next tick queue". This queue is fully drained
|
||||
* after the current operation on the JavaScript stack runs to completion
|
||||
* and before the event loop is allowed to continue.
|
||||
*
|
||||
* @param {Function} callback
|
||||
* @param {any[]} args
|
||||
*/
|
||||
function nextTick(callback, ...args) {
|
||||
return process.nextTick(callback, ...args);
|
||||
},
|
||||
|
||||
cwd:
|
||||
/**
|
||||
* @returns the current working directory.
|
||||
*/
|
||||
function () {
|
||||
return process.cwd();
|
||||
},
|
||||
|
||||
getuid:
|
||||
/**
|
||||
* @returns the numeric user identity of the process
|
||||
*/
|
||||
function () {
|
||||
return process.getuid();
|
||||
},
|
||||
|
||||
getProcessMemoryInfo:
|
||||
/**
|
||||
* @returns {Promise<import('electron').ProcessMemoryInfo>}
|
||||
*/
|
||||
function () {
|
||||
return process.getProcessMemoryInfo();
|
||||
},
|
||||
|
||||
on:
|
||||
/**
|
||||
* @param {string} type
|
||||
* @param {() => void} callback
|
||||
*/
|
||||
function (type, callback) {
|
||||
if (validateProcessEventType(type)) {
|
||||
process.on(type, callback);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Some information about the context we are running in.
|
||||
*/
|
||||
context: {
|
||||
get sandbox() { return process.argv.includes('--enable-sandbox'); }
|
||||
}
|
||||
};
|
||||
|
||||
// Use `contextBridge` APIs to expose globals to VSCode
|
||||
// only if context isolation is enabled, otherwise just
|
||||
// add to the DOM global.
|
||||
let useContextBridge = process.argv.includes('--context-isolation');
|
||||
if (useContextBridge) {
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('vscode', globals);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
useContextBridge = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!useContextBridge) {
|
||||
// @ts-ignore
|
||||
window.vscode = globals;
|
||||
}
|
||||
|
||||
//#region Utilities
|
||||
|
||||
/**
|
||||
* @param {string} channel
|
||||
*/
|
||||
function validateIPC(channel) {
|
||||
if (!channel || !channel.startsWith('vscode:')) {
|
||||
throw new Error(`Unsupported event IPC channel '${channel}'`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @returns {type is 'uncaughtException'}
|
||||
*/
|
||||
function validateProcessEventType(type) {
|
||||
if (type !== 'uncaughtException') {
|
||||
throw new Error(`Unsupported process event '${type}'`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If VSCode is not run from a terminal, we should resolve additional
|
||||
* shell specific environment from the OS shell to ensure we are seeing
|
||||
* all development related environment variables. We do this from the
|
||||
* main process because it may involve spawning a shell.
|
||||
*/
|
||||
function resolveEnv() {
|
||||
return new Promise(function (resolve) {
|
||||
const handle = setTimeout(function () {
|
||||
console.warn('Preload: Unable to resolve shell environment in a reasonable time');
|
||||
|
||||
// It took too long to fetch the shell environment, return
|
||||
resolve();
|
||||
}, 3000);
|
||||
|
||||
ipcRenderer.once('vscode:acceptShellEnv', function (event, shellEnv) {
|
||||
clearTimeout(handle);
|
||||
|
||||
// Assign all keys of the shell environment to our process environment
|
||||
Object.assign(process.env, shellEnv);
|
||||
|
||||
resolve();
|
||||
});
|
||||
|
||||
ipcRenderer.send('vscode:fetchShellEnv');
|
||||
});
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}());
|
@ -0,0 +1,170 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
// #######################################################################
|
||||
// ### ###
|
||||
// ### electron.d.ts types we expose from electron-sandbox ###
|
||||
// ### (copied from Electron 9.x) ###
|
||||
// ### ###
|
||||
// #######################################################################
|
||||
|
||||
|
||||
export interface IpcRenderer {
|
||||
/**
|
||||
* Listens to `channel`, when a new message arrives `listener` would be called with
|
||||
* `listener(event, args...)`.
|
||||
*/
|
||||
on(channel: string, listener: (event: unknown, ...args: any[]) => void): void;
|
||||
|
||||
/**
|
||||
* Adds a one time `listener` function for the event. This `listener` is invoked
|
||||
* only the next time a message is sent to `channel`, after which it is removed.
|
||||
*/
|
||||
once(channel: string, listener: (event: unknown, ...args: any[]) => void): void;
|
||||
|
||||
/**
|
||||
* Removes the specified `listener` from the listener array for the specified
|
||||
* `channel`.
|
||||
*/
|
||||
removeListener(channel: string, listener: (event: unknown, ...args: any[]) => void): void;
|
||||
|
||||
/**
|
||||
* Send an asynchronous message to the main process via `channel`, along with
|
||||
* arguments. Arguments will be serialized with the Structured Clone Algorithm,
|
||||
* just like `postMessage`, so prototype chains will not be included. Sending
|
||||
* Functions, Promises, Symbols, WeakMaps, or WeakSets will throw an exception.
|
||||
*
|
||||
* > **NOTE**: Sending non-standard JavaScript types such as DOM objects or special
|
||||
* Electron objects is deprecated, and will begin throwing an exception starting
|
||||
* with Electron 9.
|
||||
*
|
||||
* The main process handles it by listening for `channel` with the `ipcMain`
|
||||
* module.
|
||||
*/
|
||||
send(channel: string, ...args: any[]): void;
|
||||
}
|
||||
|
||||
export interface WebFrame {
|
||||
/**
|
||||
* Changes the zoom level to the specified level. The original size is 0 and each
|
||||
* increment above or below represents zooming 20% larger or smaller to default
|
||||
* limits of 300% and 50% of original size, respectively.
|
||||
*/
|
||||
setZoomLevel(level: number): void;
|
||||
}
|
||||
|
||||
export interface CrashReporter {
|
||||
/**
|
||||
* Set an extra parameter to be sent with the crash report. The values specified
|
||||
* here will be sent in addition to any values set via the `extra` option when
|
||||
* `start` was called.
|
||||
*
|
||||
* Parameters added in this fashion (or via the `extra` parameter to
|
||||
* `crashReporter.start`) are specific to the calling process. Adding extra
|
||||
* parameters in the main process will not cause those parameters to be sent along
|
||||
* with crashes from renderer or other child processes. Similarly, adding extra
|
||||
* parameters in a renderer process will not result in those parameters being sent
|
||||
* with crashes that occur in other renderer processes or in the main process.
|
||||
*
|
||||
* **Note:** Parameters have limits on the length of the keys and values. Key names
|
||||
* must be no longer than 39 bytes, and values must be no longer than 127 bytes.
|
||||
* Keys with names longer than the maximum will be silently ignored. Key values
|
||||
* longer than the maximum length will be truncated.
|
||||
*/
|
||||
addExtraParameter(key: string, value: string): void;
|
||||
}
|
||||
|
||||
export interface ProcessMemoryInfo {
|
||||
|
||||
// Docs: http://electronjs.org/docs/api/structures/process-memory-info
|
||||
|
||||
/**
|
||||
* The amount of memory not shared by other processes, such as JS heap or HTML
|
||||
* content in Kilobytes.
|
||||
*/
|
||||
private: number;
|
||||
/**
|
||||
* The amount of memory currently pinned to actual physical RAM in Kilobytes.
|
||||
*
|
||||
* @platform linux,win32
|
||||
*/
|
||||
residentSet: number;
|
||||
/**
|
||||
* The amount of memory shared between processes, typically memory consumed by the
|
||||
* Electron code itself in Kilobytes.
|
||||
*/
|
||||
shared: number;
|
||||
}
|
||||
|
||||
export interface CrashReporterStartOptions {
|
||||
/**
|
||||
* URL that crash reports will be sent to as POST.
|
||||
*/
|
||||
submitURL: string;
|
||||
/**
|
||||
* Defaults to `app.name`.
|
||||
*/
|
||||
productName?: string;
|
||||
/**
|
||||
* Deprecated alias for `{ globalExtra: { _companyName: ... } }`.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
companyName?: string;
|
||||
/**
|
||||
* Whether crash reports should be sent to the server. If false, crash reports will
|
||||
* be collected and stored in the crashes directory, but not uploaded. Default is
|
||||
* `true`.
|
||||
*/
|
||||
uploadToServer?: boolean;
|
||||
/**
|
||||
* If true, crashes generated in the main process will not be forwarded to the
|
||||
* system crash handler. Default is `false`.
|
||||
*/
|
||||
ignoreSystemCrashHandler?: boolean;
|
||||
/**
|
||||
* If true, limit the number of crashes uploaded to 1/hour. Default is `false`.
|
||||
*
|
||||
* @platform darwin,win32
|
||||
*/
|
||||
rateLimit?: boolean;
|
||||
/**
|
||||
* If true, crash reports will be compressed and uploaded with `Content-Encoding:
|
||||
* gzip`. Not all collection servers support compressed payloads. Default is
|
||||
* `false`.
|
||||
*
|
||||
* @platform darwin,win32
|
||||
*/
|
||||
compress?: boolean;
|
||||
/**
|
||||
* Extra string key/value annotations that will be sent along with crash reports
|
||||
* that are generated in the main process. Only string values are supported.
|
||||
* Crashes generated in child processes will not contain these extra parameters to
|
||||
* crash reports generated from child processes, call `addExtraParameter` from the
|
||||
* child process.
|
||||
*/
|
||||
extra?: Record<string, string>;
|
||||
/**
|
||||
* Extra string key/value annotations that will be sent along with any crash
|
||||
* reports generated in any process. These annotations cannot be changed once the
|
||||
* crash reporter has been started. If a key is present in both the global extra
|
||||
* parameters and the process-specific extra parameters, then the global one will
|
||||
* take precedence. By default, `productName` and the app version are included, as
|
||||
* well as the Electron version.
|
||||
*/
|
||||
globalExtra?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional information around a `app.on('login')` event.
|
||||
*/
|
||||
export interface AuthInfo {
|
||||
isProxy: boolean;
|
||||
scheme: string;
|
||||
host: string;
|
||||
port: number;
|
||||
realm: string;
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { globals, INodeProcess, IProcessEnvironment } from 'vs/base/common/platform';
|
||||
import { ProcessMemoryInfo, CrashReporter, IpcRenderer, WebFrame } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes';
|
||||
|
||||
export interface ISandboxNodeProcess extends INodeProcess {
|
||||
|
||||
/**
|
||||
* The process.platform property returns a string identifying the operating system platform
|
||||
* on which the Node.js process is running.
|
||||
*/
|
||||
platform: 'win32' | 'linux' | 'darwin';
|
||||
|
||||
/**
|
||||
* The type will always be Electron renderer.
|
||||
*/
|
||||
type: 'renderer';
|
||||
|
||||
/**
|
||||
* A list of versions for the current node.js/electron configuration.
|
||||
*/
|
||||
versions: { [key: string]: string | undefined };
|
||||
|
||||
/**
|
||||
* The process.env property returns an object containing the user environment.
|
||||
*/
|
||||
env: IProcessEnvironment;
|
||||
|
||||
/**
|
||||
* The current working directory.
|
||||
*/
|
||||
cwd(): string;
|
||||
|
||||
/**
|
||||
* Returns the numeric user identity of the process.
|
||||
*/
|
||||
getuid(): number;
|
||||
|
||||
/**
|
||||
* Allows to await resolving the full process environment by checking for the shell environment
|
||||
* of the OS in certain cases (e.g. when the app is started from the Dock on macOS).
|
||||
*/
|
||||
whenEnvResolved(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Adds callback to the "next tick queue". This queue is fully drained
|
||||
* after the current operation on the JavaScript stack runs to completion
|
||||
* and before the event loop is allowed to continue.
|
||||
*/
|
||||
nextTick(callback: (...args: any[]) => void, ...args: any[]): void;
|
||||
|
||||
/**
|
||||
* A listener on the process. Only a small subset of listener types are allowed.
|
||||
*/
|
||||
on: (type: string, callback: Function) => void;
|
||||
|
||||
/**
|
||||
* Resolves with a ProcessMemoryInfo
|
||||
*
|
||||
* Returns an object giving memory usage statistics about the current process. Note
|
||||
* that all statistics are reported in Kilobytes. This api should be called after
|
||||
* app ready.
|
||||
*
|
||||
* Chromium does not provide `residentSet` value for macOS. This is because macOS
|
||||
* performs in-memory compression of pages that haven't been recently used. As a
|
||||
* result the resident set size value is not what one would expect. `private`
|
||||
* memory is more representative of the actual pre-compression memory usage of the
|
||||
* process on macOS.
|
||||
*/
|
||||
getProcessMemoryInfo: () => Promise<ProcessMemoryInfo>;
|
||||
}
|
||||
|
||||
export interface ISandboxContext {
|
||||
|
||||
/**
|
||||
* Wether the renderer runs with `sandbox` enabled or not.
|
||||
*/
|
||||
sandbox: boolean;
|
||||
}
|
||||
|
||||
export const ipcRenderer: IpcRenderer = globals.vscode.ipcRenderer;
|
||||
export const webFrame: WebFrame = globals.vscode.webFrame;
|
||||
export const crashReporter: CrashReporter = globals.vscode.crashReporter;
|
||||
export const process: ISandboxNodeProcess = globals.vscode.process;
|
||||
export const context: ISandboxContext = globals.vscode.context;
|
@ -0,0 +1,15 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { ipcRenderer, crashReporter, webFrame } from 'vs/base/parts/sandbox/electron-sandbox/globals';
|
||||
|
||||
suite('Sandbox', () => {
|
||||
test('globals', () => {
|
||||
assert.ok(ipcRenderer);
|
||||
assert.ok(crashReporter);
|
||||
assert.ok(webFrame);
|
||||
});
|
||||
});
|
314
lib/vscode/src/vs/base/parts/storage/common/storage.ts
Normal file
314
lib/vscode/src/vs/base/parts/storage/common/storage.ts
Normal file
@ -0,0 +1,314 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { ThrottledDelayer } from 'vs/base/common/async';
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
|
||||
export enum StorageHint {
|
||||
|
||||
// A hint to the storage that the storage
|
||||
// does not exist on disk yet. This allows
|
||||
// the storage library to improve startup
|
||||
// time by not checking the storage for data.
|
||||
STORAGE_DOES_NOT_EXIST
|
||||
}
|
||||
|
||||
export interface IStorageOptions {
|
||||
readonly hint?: StorageHint;
|
||||
}
|
||||
|
||||
export interface IUpdateRequest {
|
||||
readonly insert?: Map<string, string>;
|
||||
readonly delete?: Set<string>;
|
||||
}
|
||||
|
||||
export interface IStorageItemsChangeEvent {
|
||||
readonly changed?: Map<string, string>;
|
||||
readonly deleted?: Set<string>;
|
||||
}
|
||||
|
||||
export interface IStorageDatabase {
|
||||
|
||||
readonly onDidChangeItemsExternal: Event<IStorageItemsChangeEvent>;
|
||||
|
||||
getItems(): Promise<Map<string, string>>;
|
||||
updateItems(request: IUpdateRequest): Promise<void>;
|
||||
|
||||
close(recovery?: () => Map<string, string>): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IStorage extends IDisposable {
|
||||
|
||||
readonly items: Map<string, string>;
|
||||
readonly size: number;
|
||||
readonly onDidChangeStorage: Event<string>;
|
||||
|
||||
init(): Promise<void>;
|
||||
|
||||
get(key: string, fallbackValue: string): string;
|
||||
get(key: string, fallbackValue?: string): string | undefined;
|
||||
|
||||
getBoolean(key: string, fallbackValue: boolean): boolean;
|
||||
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
|
||||
|
||||
getNumber(key: string, fallbackValue: number): number;
|
||||
getNumber(key: string, fallbackValue?: number): number | undefined;
|
||||
|
||||
set(key: string, value: string | boolean | number | undefined | null): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
enum StorageState {
|
||||
None,
|
||||
Initialized,
|
||||
Closed
|
||||
}
|
||||
|
||||
export class Storage extends Disposable implements IStorage {
|
||||
|
||||
private static readonly DEFAULT_FLUSH_DELAY = 100;
|
||||
|
||||
private readonly _onDidChangeStorage = this._register(new Emitter<string>());
|
||||
readonly onDidChangeStorage = this._onDidChangeStorage.event;
|
||||
|
||||
private state = StorageState.None;
|
||||
|
||||
private cache = new Map<string, string>();
|
||||
|
||||
private readonly flushDelayer = this._register(new ThrottledDelayer<void>(Storage.DEFAULT_FLUSH_DELAY));
|
||||
|
||||
private pendingDeletes = new Set<string>();
|
||||
private pendingInserts = new Map<string, string>();
|
||||
|
||||
constructor(
|
||||
protected readonly database: IStorageDatabase,
|
||||
private readonly options: IStorageOptions = Object.create(null)
|
||||
) {
|
||||
super();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this._register(this.database.onDidChangeItemsExternal(e => this.onDidChangeItemsExternal(e)));
|
||||
}
|
||||
|
||||
private onDidChangeItemsExternal(e: IStorageItemsChangeEvent): void {
|
||||
// items that change external require us to update our
|
||||
// caches with the values. we just accept the value and
|
||||
// emit an event if there is a change.
|
||||
e.changed?.forEach((value, key) => this.accept(key, value));
|
||||
e.deleted?.forEach(key => this.accept(key, undefined));
|
||||
}
|
||||
|
||||
private accept(key: string, value: string | undefined): void {
|
||||
if (this.state === StorageState.Closed) {
|
||||
return; // Return early if we are already closed
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
|
||||
// Item got removed, check for deletion
|
||||
if (isUndefinedOrNull(value)) {
|
||||
changed = this.cache.delete(key);
|
||||
}
|
||||
|
||||
// Item got updated, check for change
|
||||
else {
|
||||
const currentValue = this.cache.get(key);
|
||||
if (currentValue !== value) {
|
||||
this.cache.set(key, value);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Signal to outside listeners
|
||||
if (changed) {
|
||||
this._onDidChangeStorage.fire(key);
|
||||
}
|
||||
}
|
||||
|
||||
get items(): Map<string, string> {
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.cache.size;
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
if (this.state !== StorageState.None) {
|
||||
return; // either closed or already initialized
|
||||
}
|
||||
|
||||
this.state = StorageState.Initialized;
|
||||
|
||||
if (this.options.hint === StorageHint.STORAGE_DOES_NOT_EXIST) {
|
||||
// return early if we know the storage file does not exist. this is a performance
|
||||
// optimization to not load all items of the underlying storage if we know that
|
||||
// there can be no items because the storage does not exist.
|
||||
return;
|
||||
}
|
||||
|
||||
this.cache = await this.database.getItems();
|
||||
}
|
||||
|
||||
get(key: string, fallbackValue: string): string;
|
||||
get(key: string, fallbackValue?: string): string | undefined;
|
||||
get(key: string, fallbackValue?: string): string | undefined {
|
||||
const value = this.cache.get(key);
|
||||
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
getBoolean(key: string, fallbackValue: boolean): boolean;
|
||||
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
|
||||
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined {
|
||||
const value = this.get(key);
|
||||
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
return value === 'true';
|
||||
}
|
||||
|
||||
getNumber(key: string, fallbackValue: number): number;
|
||||
getNumber(key: string, fallbackValue?: number): number | undefined;
|
||||
getNumber(key: string, fallbackValue?: number): number | undefined {
|
||||
const value = this.get(key);
|
||||
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
return parseInt(value, 10);
|
||||
}
|
||||
|
||||
set(key: string, value: string | boolean | number | null | undefined): Promise<void> {
|
||||
if (this.state === StorageState.Closed) {
|
||||
return Promise.resolve(); // Return early if we are already closed
|
||||
}
|
||||
|
||||
// We remove the key for undefined/null values
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return this.delete(key);
|
||||
}
|
||||
|
||||
// Otherwise, convert to String and store
|
||||
const valueStr = String(value);
|
||||
|
||||
// Return early if value already set
|
||||
const currentValue = this.cache.get(key);
|
||||
if (currentValue === valueStr) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Update in cache and pending
|
||||
this.cache.set(key, valueStr);
|
||||
this.pendingInserts.set(key, valueStr);
|
||||
this.pendingDeletes.delete(key);
|
||||
|
||||
// Event
|
||||
this._onDidChangeStorage.fire(key);
|
||||
|
||||
// Accumulate work by scheduling after timeout
|
||||
return this.flushDelayer.trigger(() => this.flushPending());
|
||||
}
|
||||
|
||||
delete(key: string): Promise<void> {
|
||||
if (this.state === StorageState.Closed) {
|
||||
return Promise.resolve(); // Return early if we are already closed
|
||||
}
|
||||
|
||||
// Remove from cache and add to pending
|
||||
const wasDeleted = this.cache.delete(key);
|
||||
if (!wasDeleted) {
|
||||
return Promise.resolve(); // Return early if value already deleted
|
||||
}
|
||||
|
||||
if (!this.pendingDeletes.has(key)) {
|
||||
this.pendingDeletes.add(key);
|
||||
}
|
||||
|
||||
this.pendingInserts.delete(key);
|
||||
|
||||
// Event
|
||||
this._onDidChangeStorage.fire(key);
|
||||
|
||||
// Accumulate work by scheduling after timeout
|
||||
return this.flushDelayer.trigger(() => this.flushPending());
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (this.state === StorageState.Closed) {
|
||||
return Promise.resolve(); // return if already closed
|
||||
}
|
||||
|
||||
// Update state
|
||||
this.state = StorageState.Closed;
|
||||
|
||||
// Trigger new flush to ensure data is persisted and then close
|
||||
// even if there is an error flushing. We must always ensure
|
||||
// the DB is closed to avoid corruption.
|
||||
//
|
||||
// Recovery: we pass our cache over as recovery option in case
|
||||
// the DB is not healthy.
|
||||
try {
|
||||
await this.flushDelayer.trigger(() => this.flushPending(), 0 /* as soon as possible */);
|
||||
} catch (error) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
await this.database.close(() => this.cache);
|
||||
}
|
||||
|
||||
private flushPending(): Promise<void> {
|
||||
if (this.pendingInserts.size === 0 && this.pendingDeletes.size === 0) {
|
||||
return Promise.resolve(); // return early if nothing to do
|
||||
}
|
||||
|
||||
// Get pending data
|
||||
const updateRequest: IUpdateRequest = { insert: this.pendingInserts, delete: this.pendingDeletes };
|
||||
|
||||
// Reset pending data for next run
|
||||
this.pendingDeletes = new Set<string>();
|
||||
this.pendingInserts = new Map<string, string>();
|
||||
|
||||
// Update in storage
|
||||
return this.database.updateItems(updateRequest);
|
||||
}
|
||||
}
|
||||
|
||||
export class InMemoryStorageDatabase implements IStorageDatabase {
|
||||
|
||||
readonly onDidChangeItemsExternal = Event.None;
|
||||
|
||||
private readonly items = new Map<string, string>();
|
||||
|
||||
async getItems(): Promise<Map<string, string>> {
|
||||
return this.items;
|
||||
}
|
||||
|
||||
async updateItems(request: IUpdateRequest): Promise<void> {
|
||||
if (request.insert) {
|
||||
request.insert.forEach((value, key) => this.items.set(key, value));
|
||||
}
|
||||
|
||||
if (request.delete) {
|
||||
request.delete.forEach(key => this.items.delete(key));
|
||||
}
|
||||
}
|
||||
|
||||
async close(): Promise<void> { }
|
||||
}
|
443
lib/vscode/src/vs/base/parts/storage/node/storage.ts
Normal file
443
lib/vscode/src/vs/base/parts/storage/node/storage.ts
Normal file
@ -0,0 +1,443 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type { Database, Statement } from 'vscode-sqlite3';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { mapToString, setToString } from 'vs/base/common/map';
|
||||
import { basename } from 'vs/base/common/path';
|
||||
import { copy, renameIgnoreError, unlink } from 'vs/base/node/pfs';
|
||||
import { IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage';
|
||||
|
||||
interface IDatabaseConnection {
|
||||
readonly db: Database;
|
||||
readonly isInMemory: boolean;
|
||||
|
||||
isErroneous?: boolean;
|
||||
lastError?: string;
|
||||
}
|
||||
|
||||
export interface ISQLiteStorageDatabaseOptions {
|
||||
readonly logging?: ISQLiteStorageDatabaseLoggingOptions;
|
||||
}
|
||||
|
||||
export interface ISQLiteStorageDatabaseLoggingOptions {
|
||||
logError?: (error: string | Error) => void;
|
||||
logTrace?: (msg: string) => void;
|
||||
}
|
||||
|
||||
export class SQLiteStorageDatabase implements IStorageDatabase {
|
||||
|
||||
static readonly IN_MEMORY_PATH = ':memory:';
|
||||
|
||||
get onDidChangeItemsExternal(): Event<IStorageItemsChangeEvent> { return Event.None; } // since we are the only client, there can be no external changes
|
||||
|
||||
private static readonly BUSY_OPEN_TIMEOUT = 2000; // timeout in ms to retry when opening DB fails with SQLITE_BUSY
|
||||
private static readonly MAX_HOST_PARAMETERS = 256; // maximum number of parameters within a statement
|
||||
|
||||
private readonly name = basename(this.path);
|
||||
|
||||
private readonly logger = new SQLiteStorageDatabaseLogger(this.options.logging);
|
||||
|
||||
private readonly whenConnected = this.connect(this.path);
|
||||
|
||||
constructor(private readonly path: string, private readonly options: ISQLiteStorageDatabaseOptions = Object.create(null)) { }
|
||||
|
||||
async getItems(): Promise<Map<string, string>> {
|
||||
const connection = await this.whenConnected;
|
||||
|
||||
const items = new Map<string, string>();
|
||||
|
||||
const rows = await this.all(connection, 'SELECT * FROM ItemTable');
|
||||
rows.forEach(row => items.set(row.key, row.value));
|
||||
|
||||
if (this.logger.isTracing) {
|
||||
this.logger.trace(`[storage ${this.name}] getItems(): ${items.size} rows`);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
async updateItems(request: IUpdateRequest): Promise<void> {
|
||||
const connection = await this.whenConnected;
|
||||
|
||||
return this.doUpdateItems(connection, request);
|
||||
}
|
||||
|
||||
private doUpdateItems(connection: IDatabaseConnection, request: IUpdateRequest): Promise<void> {
|
||||
if (this.logger.isTracing) {
|
||||
this.logger.trace(`[storage ${this.name}] updateItems(): insert(${request.insert ? mapToString(request.insert) : '0'}), delete(${request.delete ? setToString(request.delete) : '0'})`);
|
||||
}
|
||||
|
||||
return this.transaction(connection, () => {
|
||||
const toInsert = request.insert;
|
||||
const toDelete = request.delete;
|
||||
|
||||
// INSERT
|
||||
if (toInsert && toInsert.size > 0) {
|
||||
const keysValuesChunks: (string[])[] = [];
|
||||
keysValuesChunks.push([]); // seed with initial empty chunk
|
||||
|
||||
// Split key/values into chunks of SQLiteStorageDatabase.MAX_HOST_PARAMETERS
|
||||
// so that we can efficiently run the INSERT with as many HOST parameters as possible
|
||||
let currentChunkIndex = 0;
|
||||
toInsert.forEach((value, key) => {
|
||||
let keyValueChunk = keysValuesChunks[currentChunkIndex];
|
||||
|
||||
if (keyValueChunk.length > SQLiteStorageDatabase.MAX_HOST_PARAMETERS) {
|
||||
currentChunkIndex++;
|
||||
keyValueChunk = [];
|
||||
keysValuesChunks.push(keyValueChunk);
|
||||
}
|
||||
|
||||
keyValueChunk.push(key, value);
|
||||
});
|
||||
|
||||
keysValuesChunks.forEach(keysValuesChunk => {
|
||||
this.prepare(connection, `INSERT INTO ItemTable VALUES ${new Array(keysValuesChunk.length / 2).fill('(?,?)').join(',')}`, stmt => stmt.run(keysValuesChunk), () => {
|
||||
const keys: string[] = [];
|
||||
let length = 0;
|
||||
toInsert.forEach((value, key) => {
|
||||
keys.push(key);
|
||||
length += value.length;
|
||||
});
|
||||
|
||||
return `Keys: ${keys.join(', ')} Length: ${length}`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE
|
||||
if (toDelete && toDelete.size) {
|
||||
const keysChunks: (string[])[] = [];
|
||||
keysChunks.push([]); // seed with initial empty chunk
|
||||
|
||||
// Split keys into chunks of SQLiteStorageDatabase.MAX_HOST_PARAMETERS
|
||||
// so that we can efficiently run the DELETE with as many HOST parameters
|
||||
// as possible
|
||||
let currentChunkIndex = 0;
|
||||
toDelete.forEach(key => {
|
||||
let keyChunk = keysChunks[currentChunkIndex];
|
||||
|
||||
if (keyChunk.length > SQLiteStorageDatabase.MAX_HOST_PARAMETERS) {
|
||||
currentChunkIndex++;
|
||||
keyChunk = [];
|
||||
keysChunks.push(keyChunk);
|
||||
}
|
||||
|
||||
keyChunk.push(key);
|
||||
});
|
||||
|
||||
keysChunks.forEach(keysChunk => {
|
||||
this.prepare(connection, `DELETE FROM ItemTable WHERE key IN (${new Array(keysChunk.length).fill('?').join(',')})`, stmt => stmt.run(keysChunk), () => {
|
||||
const keys: string[] = [];
|
||||
toDelete.forEach(key => {
|
||||
keys.push(key);
|
||||
});
|
||||
|
||||
return `Keys: ${keys.join(', ')}`;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async close(recovery?: () => Map<string, string>): Promise<void> {
|
||||
this.logger.trace(`[storage ${this.name}] close()`);
|
||||
|
||||
const connection = await this.whenConnected;
|
||||
|
||||
return this.doClose(connection, recovery);
|
||||
}
|
||||
|
||||
private doClose(connection: IDatabaseConnection, recovery?: () => Map<string, string>): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
connection.db.close(closeError => {
|
||||
if (closeError) {
|
||||
this.handleSQLiteError(connection, `[storage ${this.name}] close(): ${closeError}`);
|
||||
}
|
||||
|
||||
// Return early if this storage was created only in-memory
|
||||
// e.g. when running tests we do not need to backup.
|
||||
if (this.path === SQLiteStorageDatabase.IN_MEMORY_PATH) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
// If the DB closed successfully and we are not running in-memory
|
||||
// and the DB did not get errors during runtime, make a backup
|
||||
// of the DB so that we can use it as fallback in case the actual
|
||||
// DB becomes corrupt in the future.
|
||||
if (!connection.isErroneous && !connection.isInMemory) {
|
||||
return this.backup().then(resolve, error => {
|
||||
this.logger.error(`[storage ${this.name}] backup(): ${error}`);
|
||||
|
||||
return resolve(); // ignore failing backup
|
||||
});
|
||||
}
|
||||
|
||||
// Recovery: if we detected errors while using the DB or we are using
|
||||
// an inmemory DB (as a fallback to not being able to open the DB initially)
|
||||
// and we have a recovery function provided, we recreate the DB with this
|
||||
// data to recover all known data without loss if possible.
|
||||
if (typeof recovery === 'function') {
|
||||
|
||||
// Delete the existing DB. If the path does not exist or fails to
|
||||
// be deleted, we do not try to recover anymore because we assume
|
||||
// that the path is no longer writeable for us.
|
||||
return unlink(this.path).then(() => {
|
||||
|
||||
// Re-open the DB fresh
|
||||
return this.doConnect(this.path).then(recoveryConnection => {
|
||||
const closeRecoveryConnection = () => {
|
||||
return this.doClose(recoveryConnection, undefined /* do not attempt to recover again */);
|
||||
};
|
||||
|
||||
// Store items
|
||||
return this.doUpdateItems(recoveryConnection, { insert: recovery() }).then(() => closeRecoveryConnection(), error => {
|
||||
|
||||
// In case of an error updating items, still ensure to close the connection
|
||||
// to prevent SQLITE_BUSY errors when the connection is reestablished
|
||||
closeRecoveryConnection();
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
});
|
||||
}).then(resolve, reject);
|
||||
}
|
||||
|
||||
// Finally without recovery we just reject
|
||||
return reject(closeError || new Error('Database has errors or is in-memory without recovery option'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private backup(): Promise<void> {
|
||||
const backupPath = this.toBackupPath(this.path);
|
||||
|
||||
return copy(this.path, backupPath);
|
||||
}
|
||||
|
||||
private toBackupPath(path: string): string {
|
||||
return `${path}.backup`;
|
||||
}
|
||||
|
||||
async checkIntegrity(full: boolean): Promise<string> {
|
||||
this.logger.trace(`[storage ${this.name}] checkIntegrity(full: ${full})`);
|
||||
|
||||
const connection = await this.whenConnected;
|
||||
const row = await this.get(connection, full ? 'PRAGMA integrity_check' : 'PRAGMA quick_check');
|
||||
|
||||
const integrity = full ? (row as any)['integrity_check'] : (row as any)['quick_check'];
|
||||
|
||||
if (connection.isErroneous) {
|
||||
return `${integrity} (last error: ${connection.lastError})`;
|
||||
}
|
||||
|
||||
if (connection.isInMemory) {
|
||||
return `${integrity} (in-memory!)`;
|
||||
}
|
||||
|
||||
return integrity;
|
||||
}
|
||||
|
||||
private async connect(path: string, retryOnBusy: boolean = true): Promise<IDatabaseConnection> {
|
||||
this.logger.trace(`[storage ${this.name}] open(${path}, retryOnBusy: ${retryOnBusy})`);
|
||||
|
||||
try {
|
||||
return await this.doConnect(path);
|
||||
} catch (error) {
|
||||
this.logger.error(`[storage ${this.name}] open(): Unable to open DB due to ${error}`);
|
||||
|
||||
// SQLITE_BUSY should only arise if another process is locking the same DB we want
|
||||
// to open at that time. This typically never happens because a DB connection is
|
||||
// limited per window. However, in the event of a window reload, it may be possible
|
||||
// that the previous connection was not properly closed while the new connection is
|
||||
// already established.
|
||||
//
|
||||
// In this case we simply wait for some time and retry once to establish the connection.
|
||||
//
|
||||
if (error.code === 'SQLITE_BUSY' && retryOnBusy) {
|
||||
await timeout(SQLiteStorageDatabase.BUSY_OPEN_TIMEOUT);
|
||||
|
||||
return this.connect(path, false /* not another retry */);
|
||||
}
|
||||
|
||||
// Otherwise, best we can do is to recover from a backup if that exists, as such we
|
||||
// move the DB to a different filename and try to load from backup. If that fails,
|
||||
// a new empty DB is being created automatically.
|
||||
//
|
||||
// The final fallback is to use an in-memory DB which should only happen if the target
|
||||
// folder is really not writeable for us.
|
||||
//
|
||||
try {
|
||||
await unlink(path);
|
||||
await renameIgnoreError(this.toBackupPath(path), path);
|
||||
|
||||
return await this.doConnect(path);
|
||||
} catch (error) {
|
||||
this.logger.error(`[storage ${this.name}] open(): Unable to use backup due to ${error}`);
|
||||
|
||||
// In case of any error to open the DB, use an in-memory
|
||||
// DB so that we always have a valid DB to talk to.
|
||||
return this.doConnect(SQLiteStorageDatabase.IN_MEMORY_PATH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleSQLiteError(connection: IDatabaseConnection, msg: string): void {
|
||||
connection.isErroneous = true;
|
||||
connection.lastError = msg;
|
||||
|
||||
this.logger.error(msg);
|
||||
}
|
||||
|
||||
private doConnect(path: string): Promise<IDatabaseConnection> {
|
||||
return new Promise((resolve, reject) => {
|
||||
import('vscode-sqlite3').then(sqlite3 => {
|
||||
const connection: IDatabaseConnection = {
|
||||
db: new (this.logger.isTracing ? sqlite3.verbose().Database : sqlite3.Database)(path, error => {
|
||||
if (error) {
|
||||
return connection.db ? connection.db.close(() => reject(error)) : reject(error);
|
||||
}
|
||||
|
||||
// The following exec() statement serves two purposes:
|
||||
// - create the DB if it does not exist yet
|
||||
// - validate that the DB is not corrupt (the open() call does not throw otherwise)
|
||||
return this.exec(connection, [
|
||||
'PRAGMA user_version = 1;',
|
||||
'CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB)'
|
||||
].join('')).then(() => {
|
||||
return resolve(connection);
|
||||
}, error => {
|
||||
return connection.db.close(() => reject(error));
|
||||
});
|
||||
}),
|
||||
isInMemory: path === SQLiteStorageDatabase.IN_MEMORY_PATH
|
||||
};
|
||||
|
||||
// Errors
|
||||
connection.db.on('error', error => this.handleSQLiteError(connection, `[storage ${this.name}] Error (event): ${error}`));
|
||||
|
||||
// Tracing
|
||||
if (this.logger.isTracing) {
|
||||
connection.db.on('trace', sql => this.logger.trace(`[storage ${this.name}] Trace (event): ${sql}`));
|
||||
}
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
private exec(connection: IDatabaseConnection, sql: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
connection.db.exec(sql, error => {
|
||||
if (error) {
|
||||
this.handleSQLiteError(connection, `[storage ${this.name}] exec(): ${error}`);
|
||||
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private get(connection: IDatabaseConnection, sql: string): Promise<object> {
|
||||
return new Promise((resolve, reject) => {
|
||||
connection.db.get(sql, (error, row) => {
|
||||
if (error) {
|
||||
this.handleSQLiteError(connection, `[storage ${this.name}] get(): ${error}`);
|
||||
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
return resolve(row);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private all(connection: IDatabaseConnection, sql: string): Promise<{ key: string, value: string }[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
connection.db.all(sql, (error, rows) => {
|
||||
if (error) {
|
||||
this.handleSQLiteError(connection, `[storage ${this.name}] all(): ${error}`);
|
||||
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
return resolve(rows);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private transaction(connection: IDatabaseConnection, transactions: () => void): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
connection.db.serialize(() => {
|
||||
connection.db.run('BEGIN TRANSACTION');
|
||||
|
||||
transactions();
|
||||
|
||||
connection.db.run('END TRANSACTION', error => {
|
||||
if (error) {
|
||||
this.handleSQLiteError(connection, `[storage ${this.name}] transaction(): ${error}`);
|
||||
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private prepare(connection: IDatabaseConnection, sql: string, runCallback: (stmt: Statement) => void, errorDetails: () => string): void {
|
||||
const stmt = connection.db.prepare(sql);
|
||||
|
||||
const statementErrorListener = (error: Error) => {
|
||||
this.handleSQLiteError(connection, `[storage ${this.name}] prepare(): ${error} (${sql}). Details: ${errorDetails()}`);
|
||||
};
|
||||
|
||||
stmt.on('error', statementErrorListener);
|
||||
|
||||
runCallback(stmt);
|
||||
|
||||
stmt.finalize(error => {
|
||||
if (error) {
|
||||
statementErrorListener(error);
|
||||
}
|
||||
|
||||
stmt.removeListener('error', statementErrorListener);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class SQLiteStorageDatabaseLogger {
|
||||
private readonly logTrace: ((msg: string) => void) | undefined;
|
||||
private readonly logError: ((error: string | Error) => void) | undefined;
|
||||
|
||||
constructor(options?: ISQLiteStorageDatabaseLoggingOptions) {
|
||||
if (options && typeof options.logTrace === 'function') {
|
||||
this.logTrace = options.logTrace;
|
||||
}
|
||||
|
||||
if (options && typeof options.logError === 'function') {
|
||||
this.logError = options.logError;
|
||||
}
|
||||
}
|
||||
|
||||
get isTracing(): boolean {
|
||||
return !!this.logTrace;
|
||||
}
|
||||
|
||||
trace(msg: string): void {
|
||||
if (this.logTrace) {
|
||||
this.logTrace(msg);
|
||||
}
|
||||
}
|
||||
|
||||
error(error: string | Error): void {
|
||||
if (this.logError) {
|
||||
this.logError(error);
|
||||
}
|
||||
}
|
||||
}
|
816
lib/vscode/src/vs/base/parts/storage/test/node/storage.test.ts
Normal file
816
lib/vscode/src/vs/base/parts/storage/test/node/storage.test.ts
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user