Archived
1
0

chore(vscode): update to 1.53.2

These conflicts will be resolved in the following commits. We do it this way so
that PR review is possible.
This commit is contained in:
Joe Previte
2021-02-25 11:27:27 -07:00
1900 changed files with 83066 additions and 64589 deletions

View File

@ -110,13 +110,13 @@ export const onDidChangeFullscreen = WindowManager.INSTANCE.onDidChangeFullscree
const userAgent = navigator.userAgent;
export const isEdge = (userAgent.indexOf('Edge/') >= 0);
export const isOpera = (userAgent.indexOf('Opera') >= 0);
export const isEdgeLegacy = (userAgent.indexOf('Edge/') >= 0);
export const isFirefox = (userAgent.indexOf('Firefox') >= 0);
export const isWebKit = (userAgent.indexOf('AppleWebKit') >= 0);
export const isChrome = (userAgent.indexOf('Chrome') >= 0);
export const isSafari = (!isChrome && (userAgent.indexOf('Safari') >= 0));
export const isWebkitWebView = (!isChrome && !isSafari && isWebKit);
export const isIPad = (userAgent.indexOf('iPad') >= 0 || (isSafari && navigator.maxTouchPoints > 0));
export const isEdgeWebView = isEdge && (userAgent.indexOf('WebView/') >= 0);
export const isEdgeLegacyWebView = isEdgeLegacy && (userAgent.indexOf('WebView/') >= 0);
export const isElectron = (userAgent.indexOf('Electron/') >= 0);
export const isStandalone = (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches);

View File

@ -27,7 +27,7 @@ export const BrowserFeatures = {
|| !!(navigator && navigator.clipboard && navigator.clipboard.readText)
),
richText: (() => {
if (browser.isEdge) {
if (browser.isEdgeLegacy) {
let index = navigator.userAgent.indexOf('Edge/');
let version = parseInt(navigator.userAgent.substring(index + 5, navigator.userAgent.indexOf('.', index)), 10);

View File

@ -16,7 +16,7 @@ export interface IContextMenuEvent {
export interface IContextMenuDelegate {
getAnchor(): HTMLElement | { x: number; y: number; width?: number; height?: number; };
getActions(): IAction[];
getActions(): readonly IAction[];
getCheckedActionsRepresentation?(action: IAction): 'radio' | 'checkbox';
getActionViewItem?(action: IAction): IActionViewItem | undefined;
getActionsContext?(event?: IContextMenuEvent): any;

View File

@ -20,18 +20,15 @@ import { KeyCode } from 'vs/base/common/keyCodes';
export function clearNode(node: HTMLElement): void {
while (node.firstChild) {
node.removeChild(node.firstChild);
node.firstChild.remove();
}
}
/**
* @deprecated Use node.isConnected directly
*/
export function isInDOM(node: Node | null): boolean {
while (node) {
if (node === document.body) {
return true;
}
node = node.parentNode || (node as ShadowRoot).host;
}
return false;
return node?.isConnected ?? false;
}
class DomListener implements IDisposable {
@ -451,6 +448,8 @@ export interface IDimension {
export class Dimension implements IDimension {
static readonly None = new Dimension(0, 0);
constructor(
public readonly width: number,
public readonly height: number,
@ -842,7 +841,7 @@ export const EventType = {
MOUSE_OUT: 'mouseout',
MOUSE_ENTER: 'mouseenter',
MOUSE_LEAVE: 'mouseleave',
MOUSE_WHEEL: browser.isEdge ? 'mousewheel' : 'wheel',
MOUSE_WHEEL: browser.isEdgeLegacy ? 'mousewheel' : 'wheel',
POINTER_UP: 'pointerup',
POINTER_DOWN: 'pointerdown',
POINTER_MOVE: 'pointermove',
@ -1002,9 +1001,13 @@ export function after<T extends Node>(sibling: HTMLElement, child: T): T {
return child;
}
export function append<T extends Node>(parent: HTMLElement, ...children: T[]): T {
children.forEach(child => parent.appendChild(child));
return children[children.length - 1];
export function append<T extends Node>(parent: HTMLElement, child: T): T;
export function append<T extends Node>(parent: HTMLElement, ...children: (T | string)[]): void;
export function append<T extends Node>(parent: HTMLElement, ...children: (T | string)[]): T | void {
parent.append(...children);
if (children.length === 1 && typeof children[0] !== 'string') {
return <T>children[0];
}
}
export function prepend<T extends Node>(parent: HTMLElement, child: T): T {
@ -1012,26 +1015,12 @@ export function prepend<T extends Node>(parent: HTMLElement, child: T): T {
return child;
}
/**
* Removes all children from `parent` and appends `children`
*/
export function reset(parent: HTMLElement, ...children: Array<Node | string>): void {
parent.innerText = '';
appendChildren(parent, ...children);
}
/**
* Appends `children` to `parent`
*/
export function appendChildren(parent: HTMLElement, ...children: Array<Node | string>): void {
for (const child of children) {
if (child instanceof Node) {
parent.appendChild(child);
} else if (typeof child === 'string') {
parent.appendChild(document.createTextNode(child));
}
}
append(parent, ...children);
}
const SELECTOR_REGEX = /([\w\-]+)?(#([\w\-]+))?((\.([\w\-]+))*)/;
@ -1085,13 +1074,7 @@ function _$<T extends Element>(namespace: Namespace, description: string, attrs?
}
});
for (const child of children) {
if (child instanceof Node) {
result.appendChild(child);
} else if (typeof child === 'string') {
result.appendChild(document.createTextNode(child));
}
}
result.append(...children);
return result as T;
}
@ -1211,9 +1194,10 @@ export function computeScreenAwareSize(cssPx: number): number {
* See https://mathiasbynens.github.io/rel-noopener/
*/
export function windowOpenNoOpener(url: string): void {
if (platform.isNative || browser.isEdgeWebView) {
if (browser.isElectron || browser.isEdgeLegacyWebView) {
// In VSCode, window.open() always returns null...
// The same is true for a WebView (see https://github.com/microsoft/monaco-editor/issues/628)
// Also call directly window.open in sandboxed Electron (see https://github.com/microsoft/monaco-editor/issues/2220)
window.open(url);
} else {
let newTab = window.open();
@ -1249,7 +1233,7 @@ export function asCSSUrl(uri: URI): string {
export function triggerDownload(dataOrUri: Uint8Array | URI, name: string): void {
// If the data is provided as Buffer, we create a
// blog URL out of it to produce a valid link
// blob URL out of it to produce a valid link
let url: string;
if (URI.isUri(dataOrUri)) {
url = dataOrUri.toString(true);
@ -1298,7 +1282,7 @@ export interface IDetectedFullscreen {
mode: DetectedFullscreenMode;
/**
* Wether we know for sure that we are in fullscreen mode or
* Whether we know for sure that we are in fullscreen mode or
* it is a guess.
*/
guess: boolean;
@ -1385,7 +1369,7 @@ export function safeInnerHtml(node: HTMLElement, value: string): void {
}, ['class', 'id', 'role', 'tabindex']);
const html = _ttpSafeInnerHtml?.createHTML(value, options) ?? insane(value, options);
node.innerHTML = html as unknown as string;
node.innerHTML = html as string;
}
/**
@ -1398,7 +1382,12 @@ function toBinary(str: string): string {
for (let i = 0; i < codeUnits.length; i++) {
codeUnits[i] = str.charCodeAt(i);
}
return String.fromCharCode(...new Uint8Array(codeUnits.buffer));
let binary = '';
const uint8array = new Uint8Array(codeUnits.buffer);
for (let i = 0; i < uint8array.length; i++) {
binary += String.fromCharCode(uint8array[i]);
}
return binary;
}
/**
@ -1445,7 +1434,7 @@ export namespace WebFileSystemAccess {
}
export function supported(obj: any & Window): obj is FileSystemAccess {
const candidate = obj as FileSystemAccess;
const candidate = obj as FileSystemAccess | undefined;
if (typeof candidate?.showDirectoryPicker === 'function') {
return true;
}
@ -1483,7 +1472,13 @@ export class ModifierKeyEmitter extends Emitter<IModifierKeyStatus> {
};
this._subscriptions.add(domEvent(document.body, 'keydown', true)(e => {
const event = new StandardKeyboardEvent(e);
// If Alt-key keydown event is repeated, ignore it #112347
// Only known to be necessary for Alt-Key at the moment #115810
if (event.keyCode === KeyCode.Alt && e.repeat) {
return;
}
if (e.altKey && !this._keyStatus.altKey) {
this._keyStatus.lastKeyPressed = 'alt';
@ -1595,3 +1590,9 @@ export class ModifierKeyEmitter extends Emitter<IModifierKeyStatus> {
this._subscriptions.dispose();
}
}
export function getCookieValue(name: string): string | undefined {
const match = document.cookie.match('(^|[^;]+)\\s*' + name + '\\s*=\\s*([^;]+)'); // See https://stackoverflow.com/a/25490531
return match ? match.pop() : undefined;
}

View File

@ -10,7 +10,14 @@ export async function sha1Hex(str: string): Promise<string> {
// Prefer to use browser's crypto module
if (globalThis?.crypto?.subtle) {
const hash = await globalThis.crypto.subtle.digest({ name: 'sha-1' }, VSBuffer.fromString(str).buffer);
// Careful to use `dontUseNodeBuffer` when passing the
// buffer to the browser `crypto` API. Users reported
// native crashes in certain cases that we could trace
// back to passing node.js `Buffer` around
// (https://github.com/microsoft/vscode/issues/114227)
const buffer = VSBuffer.fromString(str, { dontUseNodeBuffer: true }).buffer;
const hash = await globalThis.crypto.subtle.digest({ name: 'sha-1' }, buffer);
return toHexString(hash);
}

View File

@ -15,10 +15,10 @@ import { cloneAndChange } from 'vs/base/common/objects';
import { escape } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { FileAccess, Schemas } from 'vs/base/common/network';
import { markdownEscapeEscapedCodicons } from 'vs/base/common/codicons';
import { markdownEscapeEscapedIcons } from 'vs/base/common/iconLabels';
import { resolvePath } from 'vs/base/common/resources';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { renderCodicons } from 'vs/base/browser/codicons';
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
import { Event } from 'vs/base/common/event';
import { domEvent } from 'vs/base/browser/event';
@ -156,7 +156,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
};
renderer.paragraph = (text): string => {
if (markdown.supportThemeIcons) {
const elements = renderCodicons(text);
const elements = renderLabelWithIcons(text);
text = elements.map(e => typeof e === 'string' ? e : e.outerHTML).join('');
}
return `<p>${text}</p>`;
@ -218,7 +218,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
// We always pass the output through insane after this so that we don't rely on
// marked for sanitization.
markedOptions.sanitizer = (html: string): string => {
const match = markdown.isTrusted ? html.match(/^(<span[^<]+>)|(<\/\s*span>)$/) : undefined;
const match = markdown.isTrusted ? html.match(/^(<span[^>]+>)|(<\/\s*span>)$/) : undefined;
return match ? html : '';
};
markedOptions.sanitize = true;
@ -233,13 +233,13 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
}
// escape theme icons
if (markdown.supportThemeIcons) {
value = markdownEscapeEscapedCodicons(value);
value = markdownEscapeEscapedIcons(value);
}
const renderedMarkdown = marked.parse(value, markedOptions);
// sanitize with insane
element.innerHTML = sanitizeRenderedMarkdown(markdown, renderedMarkdown);
element.innerHTML = sanitizeRenderedMarkdown(markdown, renderedMarkdown) as string;
// signal that async code blocks can be now be inserted
signalInnerHTML!();
@ -261,13 +261,9 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
function sanitizeRenderedMarkdown(
options: { isTrusted?: boolean },
renderedMarkdown: string,
): string {
): string | TrustedHTML {
const insaneOptions = getInsaneOptions(options);
if (_ttpInsane) {
return _ttpInsane.createHTML(renderedMarkdown, insaneOptions) as unknown as string;
} else {
return insane(renderedMarkdown, insaneOptions);
}
return _ttpInsane?.createHTML(renderedMarkdown, insaneOptions) ?? insane(renderedMarkdown, insaneOptions);
}
function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOptions {
@ -302,12 +298,12 @@ function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOpti
'td': ['align']
},
filter(token: { tag: string; attrs: { readonly [key: string]: string; }; }): boolean {
if (token.tag === 'span' && options.isTrusted && (Object.keys(token.attrs).length === 1)) {
if (token.attrs['style']) {
if (token.tag === 'span' && options.isTrusted) {
if (token.attrs['style'] && (Object.keys(token.attrs).length === 1)) {
return !!token.attrs['style'].match(/^(color\:#[0-9a-fA-F]+;)?(background-color\:#[0-9a-fA-F]+;)?$/);
} else if (token.attrs['class']) {
// The class should match codicon rendering in src\vs\base\common\codicons.ts
return !!token.attrs['class'].match(/^codicon codicon-[a-z\-]+( codicon-animation-[a-z\-]+)?$/);
return !!token.attrs['class'].match(/^codicon codicon-[a-z\-]+( codicon-modifier-[a-z\-]+)?$/);
}
return false;
}

View File

@ -104,7 +104,3 @@
.monaco-action-bar .action-item.action-dropdown-item > .action-label {
margin-right: 1px;
}
.monaco-action-bar .action-item.action-dropdown-item > .monaco-dropdown {
margin-right: 4px;
}

View File

@ -20,27 +20,6 @@ export abstract class BreadcrumbsItem {
abstract render(container: HTMLElement): void;
}
export class SimpleBreadcrumbsItem extends BreadcrumbsItem {
constructor(
readonly text: string,
readonly title: string = text
) {
super();
}
equals(other: this) {
return other === this || other instanceof SimpleBreadcrumbsItem && other.text === this.text && other.title === this.title;
}
render(container: HTMLElement): void {
let node = document.createElement('div');
node.title = this.title;
node.innerText = this.text;
container.appendChild(node);
}
}
export interface IBreadcrumbsWidgetStyles {
breadcrumbsBackground?: Color;
breadcrumbsForeground?: Color;
@ -333,7 +312,12 @@ export class BreadcrumbsWidget {
private _renderItem(item: BreadcrumbsItem, container: HTMLDivElement): void {
dom.clearNode(container);
container.className = '';
item.render(container);
try {
item.render(container);
} catch (err) {
container.innerText = '<<RENDER ERROR>>';
console.error(err);
}
container.tabIndex = -1;
container.setAttribute('role', 'listitem');
container.classList.add('monaco-breadcrumb-item');

View File

@ -11,15 +11,15 @@ import { mixin } from 'vs/base/common/objects';
import { Event as BaseEvent, Emitter } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Gesture, EventType as TouchEventType } from 'vs/base/browser/touch';
import { renderCodicons } from 'vs/base/browser/codicons';
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
import { addDisposableListener, IFocusTracker, EventType, EventHelper, trackFocus, reset, removeTabIndexAndUpdateFocus } from 'vs/base/browser/dom';
import { IContextMenuProvider } from 'vs/base/browser/contextmenu';
import { IAction, IActionRunner } from 'vs/base/common/actions';
import { Action, IAction, IActionRunner } from 'vs/base/common/actions';
import { CSSIcon, Codicon } from 'vs/base/common/codicons';
export interface IButtonOptions extends IButtonStyles {
readonly title?: boolean | string;
readonly supportCodicons?: boolean;
readonly supportIcons?: boolean;
readonly secondary?: boolean;
}
@ -41,7 +41,7 @@ const defaultOptions: IButtonStyles = {
export interface IButton extends IDisposable {
readonly element: HTMLElement;
readonly onDidClick: BaseEvent<Event>;
readonly onDidClick: BaseEvent<Event | undefined>;
label: string;
icon: CSSIcon;
enabled: boolean;
@ -190,8 +190,8 @@ export class Button extends Disposable implements IButton {
set label(value: string) {
this._element.classList.add('monaco-text-button');
if (this.options.supportCodicons) {
reset(this._element, ...renderCodicons(value));
if (this.options.supportIcons) {
reset(this._element, ...renderLabelWithIcons(value));
} else {
this._element.textContent = value;
}
@ -203,7 +203,7 @@ export class Button extends Disposable implements IButton {
}
set icon(icon: CSSIcon) {
this._element.classList.add(...icon.classNames.split(' '));
this._element.classList.add(...CSSIcon.asClassNameArray(icon));
}
set enabled(value: boolean) {
@ -240,10 +240,12 @@ export interface IButtonWithDropdownOptions extends IButtonOptions {
export class ButtonWithDropdown extends Disposable implements IButton {
private readonly button: Button;
private readonly action: Action;
private readonly dropdownButton: Button;
readonly element: HTMLElement;
readonly onDidClick: BaseEvent<Event>;
private readonly _onDidClick = this._register(new Emitter<Event | undefined>());
readonly onDidClick = this._onDidClick.event;
constructor(container: HTMLElement, options: IButtonWithDropdownOptions) {
super();
@ -253,15 +255,16 @@ export class ButtonWithDropdown extends Disposable implements IButton {
container.appendChild(this.element);
this.button = this._register(new Button(this.element, options));
this.onDidClick = this.button.onDidClick;
this._register(this.button.onDidClick(e => this._onDidClick.fire(e)));
this.action = this._register(new Action('primaryAction', this.button.label, undefined, true, async () => this._onDidClick.fire(undefined)));
this.dropdownButton = this._register(new Button(this.element, { ...options, title: false, supportCodicons: true }));
this.dropdownButton = this._register(new Button(this.element, { ...options, title: false, supportIcons: true }));
this.dropdownButton.element.classList.add('monaco-dropdown-button');
this.dropdownButton.icon = Codicon.dropDownButton;
this._register(this.dropdownButton.onDidClick(() => {
this._register(this.dropdownButton.onDidClick(e => {
options.contextMenuProvider.showContextMenu({
getAnchor: () => this.dropdownButton.element,
getActions: () => options.actions,
getActions: () => [this.action, ...options.actions],
actionRunner: options.actionRunner,
onHide: () => this.dropdownButton.element.setAttribute('aria-expanded', 'false')
});
@ -271,6 +274,7 @@ export class ButtonWithDropdown extends Disposable implements IButton {
set label(value: string) {
this.button.label = value;
this.action.label = value;
}
set icon(icon: CSSIcon) {

View File

@ -101,12 +101,10 @@ export class Checkbox extends Widget {
const classes = ['monaco-custom-checkbox'];
if (this._opts.icon) {
classes.push(this._opts.icon.classNames);
} else {
classes.push('codicon'); // todo@aeschli: remove once codicon fully adopted
classes.push(...CSSIcon.asClassNameArray(this._opts.icon));
}
if (this._opts.actionClassName) {
classes.push(this._opts.actionClassName);
classes.push(...this._opts.actionClassName.split(' '));
}
if (this._checked) {
classes.push('checked');
@ -114,7 +112,7 @@ export class Checkbox extends Widget {
this.domNode = document.createElement('div');
this.domNode.title = this._opts.title;
this.domNode.className = classes.join(' ');
this.domNode.classList.add(...classes);
this.domNode.tabIndex = 0;
this.domNode.setAttribute('role', 'checkbox');
this.domNode.setAttribute('aria-checked', String(this._checked));

View File

@ -1,8 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.codicon-wrench-subaction {
opacity: 0.5;
}

View 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.
*--------------------------------------------------------------------------------------------*/
.codicon-wrench-subaction {
opacity: 0.5;
}
@keyframes codicon-spin {
100% {
transform:rotate(360deg);
}
}
.codicon-sync.codicon-modifier-spin, .codicon-loading.codicon-modifier-spin{
/* Use steps to throttle FPS to reduce CPU usage */
animation: codicon-spin 1.5s steps(30) infinite;
}
.codicon-modifier-disabled {
opacity: 0.4;
}
/* custom speed & easing for loading icon */
.codicon-loading,
.codicon-tree-item-loading::before {
animation-duration: 1s !important;
animation-timing-function: cubic-bezier(0.53, 0.21, 0.29, 0.67) !important;
}

View File

@ -4,8 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./codicon/codicon';
import 'vs/css!./codicon/codicon-modifications';
import 'vs/css!./codicon/codicon-animations';
import 'vs/css!./codicon/codicon-modifiers';
import { Codicon } from 'vs/base/common/codicons';

View File

@ -182,7 +182,9 @@ export class Dialog extends Disposable {
button.label = mnemonicButtonLabel(buttonMap[index].label, true);
this._register(button.onDidClick(e => {
EventHelper.stop(e);
if (e) {
EventHelper.stop(e);
}
resolve({
button: buttonMap[index].index,
@ -307,7 +309,9 @@ export class Dialog extends Disposable {
}
}));
this.iconElement.classList.remove(...dialogErrorIcon.classNamesArray, ...dialogWarningIcon.classNamesArray, ...dialogInfoIcon.classNamesArray, ...Codicon.loading.classNamesArray);
const spinModifierClassName = 'codicon-modifier-spin';
this.iconElement.classList.remove(...dialogErrorIcon.classNamesArray, ...dialogWarningIcon.classNamesArray, ...dialogInfoIcon.classNamesArray, ...Codicon.loading.classNamesArray, spinModifierClassName);
switch (this.options.type) {
case 'error':
@ -317,7 +321,7 @@ export class Dialog extends Disposable {
this.iconElement.classList.add(...dialogWarningIcon.classNamesArray);
break;
case 'pending':
this.iconElement.classList.add(...Codicon.loading.classNamesArray, 'codicon-animation-spin');
this.iconElement.classList.add(...Codicon.loading.classNamesArray, spinModifierClassName);
break;
case 'none':
case 'info':

View File

@ -201,7 +201,7 @@ export class Dropdown extends BaseDropdown {
}
export interface IActionProvider {
getActions(): IAction[];
getActions(): readonly IAction[];
}
export interface IDropdownMenuOptions extends IBaseDropdownOptions {
@ -215,7 +215,7 @@ export interface IDropdownMenuOptions extends IBaseDropdownOptions {
export class DropdownMenu extends BaseDropdown {
private _contextMenuProvider: IContextMenuProvider;
private _menuOptions: IMenuOptions | undefined;
private _actions: IAction[] = [];
private _actions: readonly IAction[] = [];
private actionProvider?: IActionProvider;
private menuClassName: string;
private menuAsChild?: boolean;
@ -238,7 +238,7 @@ export class DropdownMenu extends BaseDropdown {
return this._menuOptions;
}
private get actions(): IAction[] {
private get actions(): readonly IAction[] {
if (this.actionProvider) {
return this.actionProvider.getActions();
}
@ -246,7 +246,7 @@ export class DropdownMenu extends BaseDropdown {
return this._actions;
}
private set actions(actions: IAction[]) {
private set actions(actions: readonly IAction[]) {
this._actions = actions;
}

View File

@ -170,7 +170,13 @@ export class ActionWithDropdownActionViewItem extends ActionViewItem {
super.render(container);
if (this.element) {
this.element.classList.add('action-dropdown-item');
this.dropdownMenuActionViewItem = new DropdownMenuActionViewItem(new Action('dropdownAction', undefined), (<IActionWithDropdownActionViewItemOptions>this.options).menuActionsOrProvider, this.contextMenuProvider, { classNames: ['dropdown', ...Codicon.dropDownButton.classNamesArray, ...(<IActionWithDropdownActionViewItemOptions>this.options).menuActionClassNames || []] });
const menuActionsProvider = {
getActions: () => {
const actionsProvider = (<IActionWithDropdownActionViewItemOptions>this.options).menuActionsOrProvider;
return [this._action, ...(Array.isArray(actionsProvider) ? actionsProvider : actionsProvider.getActions())];
}
};
this.dropdownMenuActionViewItem = new DropdownMenuActionViewItem(this._register(new Action('dropdownAction', undefined)), menuActionsProvider, this.contextMenuProvider, { classNames: ['dropdown', ...Codicon.dropDownButton.classNamesArray, ...(<IActionWithDropdownActionViewItemOptions>this.options).menuActionClassNames || []] });
this.dropdownMenuActionViewItem.render(this.element);
}
}

View File

@ -258,6 +258,10 @@ export class FindInput extends Widget {
this.onmousedown(this.inputBox.inputElement, (e) => this._onMouseDown.fire(e));
}
public get onDidChange(): Event<string> {
return this.inputBox.onDidChange;
}
public enable(): void {
this.domNode.classList.remove('disabled');
this.inputBox.enable();

View File

@ -5,7 +5,7 @@
import * as objects from 'vs/base/common/objects';
import * as dom from 'vs/base/browser/dom';
import { renderCodicons } from 'vs/base/browser/codicons';
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
export interface IHighlight {
start: number;
@ -21,7 +21,7 @@ export class HighlightedLabel {
private highlights: IHighlight[] = [];
private didEverRender: boolean = false;
constructor(container: HTMLElement, private supportCodicons: boolean) {
constructor(container: HTMLElement, private supportIcons: boolean) {
this.domNode = document.createElement('span');
this.domNode.className = 'monaco-highlighted-label';
@ -61,12 +61,12 @@ export class HighlightedLabel {
}
if (pos < highlight.start) {
const substring = this.text.substring(pos, highlight.start);
children.push(dom.$('span', undefined, ...this.supportCodicons ? renderCodicons(substring) : [substring]));
children.push(dom.$('span', undefined, ...this.supportIcons ? renderLabelWithIcons(substring) : [substring]));
pos = highlight.end;
}
const substring = this.text.substring(highlight.start, highlight.end);
const element = dom.$('span.highlight', undefined, ...this.supportCodicons ? renderCodicons(substring) : [substring]);
const element = dom.$('span.highlight', undefined, ...this.supportIcons ? renderLabelWithIcons(substring) : [substring]);
if (highlight.extraClasses) {
element.classList.add(highlight.extraClasses);
}
@ -76,7 +76,7 @@ export class HighlightedLabel {
if (pos < this.text.length) {
const substring = this.text.substring(pos,);
children.push(dom.$('span', undefined, ...this.supportCodicons ? renderCodicons(substring) : [substring]));
children.push(dom.$('span', undefined, ...this.supportIcons ? renderLabelWithIcons(substring) : [substring]));
}
dom.reset(this.domNode, ...children);

View File

@ -45,10 +45,13 @@
}
.monaco-hover hr {
box-sizing: border-box;
border-left: 0px;
border-right: 0px;
margin-top: 4px;
margin-bottom: -4px;
margin-left: -10px;
margin-right: -10px;
margin-left: -8px;
margin-right: -8px;
height: 1px;
}

View File

@ -27,7 +27,9 @@ export class HoverWidget extends Disposable {
this.contentsDomNode = document.createElement('div');
this.contentsDomNode.className = 'monaco-hover-content';
this._scrollbar = this._register(new DomScrollableElement(this.contentsDomNode, {}));
this._scrollbar = this._register(new DomScrollableElement(this.contentsDomNode, {
consumeMouseWheelIfScrollbarIsNeeded: true
}));
this.containerDomNode.appendChild(this._scrollbar.getDomNode());
}

View File

@ -20,4 +20,5 @@ export interface IHoverDelegateOptions {
export interface IHoverDelegate {
showHover(options: IHoverDelegateOptions): IDisposable | undefined;
hideHover(): void;
}

View File

@ -16,16 +16,18 @@ import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { isFunction, isString } from 'vs/base/common/types';
import { domEvent } from 'vs/base/browser/event';
import { localize } from 'vs/nls';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
export interface IIconLabelCreationOptions {
supportHighlights?: boolean;
supportDescriptionHighlights?: boolean;
supportCodicons?: boolean;
supportIcons?: boolean;
hoverDelegate?: IHoverDelegate;
}
export interface IIconLabelMarkdownString {
markdown: IMarkdownString | string | undefined | (() => Promise<IMarkdownString | string | undefined>);
markdown: IMarkdownString | string | undefined | ((token: CancellationToken) => Promise<IMarkdownString | string | undefined>);
markdownNotSupportedFallback: string | undefined;
}
@ -114,13 +116,13 @@ export class IconLabel extends Disposable {
this.descriptionContainer = this._register(new FastLabelNode(dom.append(this.labelContainer, dom.$('span.monaco-icon-description-container'))));
if (options?.supportHighlights) {
this.nameNode = new LabelWithHighlights(nameContainer, !!options.supportCodicons);
this.nameNode = new LabelWithHighlights(nameContainer, !!options.supportIcons);
} else {
this.nameNode = new Label(nameContainer);
}
if (options?.supportDescriptionHighlights) {
this.descriptionNodeFactory = () => new HighlightedLabel(dom.append(this.descriptionContainer.element, dom.$('span.label-description')), !!options.supportCodicons);
this.descriptionNodeFactory = () => new HighlightedLabel(dom.append(this.descriptionContainer.element, dom.$('span.label-description')), !!options.supportIcons);
} else {
this.descriptionNodeFactory = () => this._register(new FastLabelNode(dom.append(this.descriptionContainer.element, dom.$('span.label-description'))));
}
@ -190,29 +192,47 @@ export class IconLabel extends Disposable {
}
}
private setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString): void {
htmlElement.removeAttribute('title');
let tooltip: () => Promise<string | IMarkdownString | undefined>;
private static adjustXAndShowCustomHover(hoverOptions: IHoverDelegateOptions | undefined, mouseX: number | undefined, hoverDelegate: IHoverDelegate, isHovering: boolean) {
if (hoverOptions && isHovering) {
if (mouseX !== undefined) {
(<IHoverDelegateTarget>hoverOptions.target).x = mouseX + 10;
}
hoverDelegate.showHover(hoverOptions);
}
}
private getTooltipForCustom(markdownTooltip: string | IIconLabelMarkdownString): (token: CancellationToken) => Promise<string | IMarkdownString | undefined> {
if (isString(markdownTooltip)) {
tooltip = async () => markdownTooltip;
return async () => markdownTooltip;
} else if (isFunction(markdownTooltip.markdown)) {
tooltip = markdownTooltip.markdown;
return markdownTooltip.markdown;
} else {
const markdown = markdownTooltip.markdown;
tooltip = async () => markdown;
return async () => markdown;
}
}
private setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString): void {
htmlElement.setAttribute('title', '');
htmlElement.removeAttribute('title');
let tooltip = this.getTooltipForCustom(markdownTooltip);
// Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely.
// On Mac, the delay is 1500.
const hoverDelay = isMacintosh ? 1500 : 500;
let hoverOptions: IHoverDelegateOptions | undefined;
let mouseX: number | undefined;
let isHovering = false;
let tokenSource: CancellationTokenSource;
function mouseOver(this: HTMLElement, e: MouseEvent): any {
if (isHovering) {
return;
}
tokenSource = new CancellationTokenSource();
function mouseLeaveOrDown(this: HTMLElement, e: MouseEvent): any {
isHovering = false;
hoverOptions = undefined;
tokenSource.dispose(true);
mouseLeaveDisposable.dispose();
mouseDownDisposable.dispose();
}
@ -232,22 +252,26 @@ export class IconLabel extends Disposable {
targetElements: [this],
dispose: () => { }
};
const resolvedTooltip = await tooltip();
hoverOptions = {
text: localize('iconLabel.loading', "Loading..."),
target,
anchorPosition: AnchorPosition.BELOW
};
IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined);
if (resolvedTooltip) {
hoverOptions = {
text: resolvedTooltip,
target,
anchorPosition: AnchorPosition.BELOW
};
// awaiting the tooltip could take a while. Make sure we're still hovering.
IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
} else {
hoverDelegate.hideHover();
}
}
// awaiting the tooltip could take a while. Make sure we're still hovering.
if (hoverOptions && isHovering) {
if (mouseX !== undefined) {
(<IHoverDelegateTarget>hoverOptions.target).x = mouseX + 10;
}
hoverDelegate.showHover(hoverOptions);
}
}
mouseMoveDisposable.dispose();
}, hoverDelay);
@ -336,7 +360,7 @@ class LabelWithHighlights {
private singleLabel: HighlightedLabel | undefined = undefined;
private options: IIconLabelValueOptions | undefined;
constructor(private container: HTMLElement, private supportCodicons: boolean) { }
constructor(private container: HTMLElement, private supportIcons: boolean) { }
setLabel(label: string | string[], options?: IIconLabelValueOptions): void {
if (this.label === label && equals(this.options, options)) {
@ -350,7 +374,7 @@ class LabelWithHighlights {
if (!this.singleLabel) {
this.container.innerText = '';
this.container.classList.remove('multiple');
this.singleLabel = new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId })), this.supportCodicons);
this.singleLabel = new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId })), this.supportIcons);
}
this.singleLabel.set(label, options?.matches, undefined, options?.labelEscapeNewLines);
@ -368,7 +392,7 @@ class LabelWithHighlights {
const id = options?.domId && `${options?.domId}_${i}`;
const name = dom.$('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' });
const highlightedLabel = new HighlightedLabel(dom.append(this.container, name), this.supportCodicons);
const highlightedLabel = new HighlightedLabel(dom.append(this.container, name), this.supportIcons);
highlightedLabel.set(l, m, undefined, options?.labelEscapeNewLines);
if (i < label.length - 1) {

View File

@ -4,21 +4,22 @@
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import { CSSIcon } from 'vs/base/common/codicons';
const renderCodiconsRegex = /(\\)?\$\((([a-z0-9\-]+?)(?:~([a-z0-9\-]*?))?)\)/gi;
const labelWithIconsRegex = /(\\)?\$\(([a-z\-]+(?:~[a-z\-]+)?)\)/gi;
export function renderCodicons(text: string): Array<HTMLSpanElement | string> {
export function renderLabelWithIcons(text: string): Array<HTMLSpanElement | string> {
const elements = new Array<HTMLSpanElement | string>();
let match: RegExpMatchArray | null;
let textStart = 0, textStop = 0;
while ((match = renderCodiconsRegex.exec(text)) !== null) {
while ((match = labelWithIconsRegex.exec(text)) !== null) {
textStop = match.index || 0;
elements.push(text.substring(textStart, textStop));
textStart = (match.index || 0) + match[0].length;
const [, escaped, codicon, name, animation] = match;
elements.push(escaped ? `$(${codicon})` : renderCodicon(name, animation));
const [, escaped, codicon] = match;
elements.push(escaped ? `$(${codicon})` : renderIcon({ id: codicon }));
}
if (textStart < text.length) {
@ -27,6 +28,8 @@ export function renderCodicons(text: string): Array<HTMLSpanElement | string> {
return elements;
}
export function renderCodicon(name: string, animation: string): HTMLSpanElement {
return dom.$(`span.codicon.codicon-${name}${animation ? `.codicon-animation-${animation}` : ''}`);
export function renderIcon(icon: CSSIcon): HTMLSpanElement {
const node = dom.$(`span`);
node.classList.add(...CSSIcon.asClassNameArray(icon));
return node;
}

View File

@ -55,6 +55,10 @@
white-space: pre; /* enable to show labels that include multiple whitespaces */
}
.monaco-icon-label.nowrap > .monaco-icon-label-container > .monaco-icon-description-container > .label-description{
white-space: nowrap
}
.vs .monaco-icon-label > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
opacity: .95;
}
@ -64,6 +68,16 @@
font-style: italic;
}
.monaco-icon-label.deprecated {
text-decoration: line-through;
opacity: 0.66;
}
/* make sure apply italic font style to decorations as well */
.monaco-icon-label.italic::after {
font-style: italic;
}
.monaco-icon-label.strikethrough > .monaco-icon-label-container > .monaco-icon-name-container > .label-name,
.monaco-icon-label.strikethrough > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
text-decoration: line-through;

View File

@ -4,16 +4,16 @@
*--------------------------------------------------------------------------------------------*/
import { reset } from 'vs/base/browser/dom';
import { renderCodicons } from 'vs/base/browser/codicons';
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
export class CodiconLabel {
export class SimpleIconLabel {
constructor(
private readonly _container: HTMLElement
) { }
set text(text: string) {
reset(this._container, ...renderCodicons(text ?? ''));
reset(this._container, ...renderLabelWithIcons(text ?? ''));
}
set title(title: string) {

View File

@ -17,20 +17,20 @@
border: 1px solid transparent;
}
.monaco-inputbox > .wrapper > .input,
.monaco-inputbox > .wrapper > .mirror {
.monaco-inputbox > .ibwrapper > .input,
.monaco-inputbox > .ibwrapper > .mirror {
/* Customizable */
padding: 4px;
}
.monaco-inputbox > .wrapper {
.monaco-inputbox > .ibwrapper {
position: relative;
width: 100%;
height: 100%;
}
.monaco-inputbox > .wrapper > .input {
.monaco-inputbox > .ibwrapper > .input {
display: inline-block;
box-sizing: border-box;
width: 100%;
@ -43,26 +43,26 @@
color: inherit;
}
.monaco-inputbox > .wrapper > input {
.monaco-inputbox > .ibwrapper > input {
text-overflow: ellipsis;
}
.monaco-inputbox > .wrapper > textarea.input {
.monaco-inputbox > .ibwrapper > textarea.input {
display: block;
-ms-overflow-style: none; /* IE 10+: hide scrollbars */
scrollbar-width: none; /* Firefox: hide scrollbars */
outline: none;
}
.monaco-inputbox > .wrapper > textarea.input::-webkit-scrollbar {
.monaco-inputbox > .ibwrapper > textarea.input::-webkit-scrollbar {
display: none; /* Chrome + Safari: hide scrollbar */
}
.monaco-inputbox > .wrapper > textarea.input.empty {
.monaco-inputbox > .ibwrapper > textarea.input.empty {
white-space: nowrap;
}
.monaco-inputbox > .wrapper > .mirror {
.monaco-inputbox > .ibwrapper > .mirror {
position: absolute;
display: inline-block;
width: 100%;

View File

@ -157,7 +157,7 @@ export class InputBox extends Widget {
let tagName = this.options.flexibleHeight ? 'textarea' : 'input';
let wrapper = dom.append(this.element, $('.wrapper'));
let wrapper = dom.append(this.element, $('.ibwrapper'));
this.input = dom.append(wrapper, $(tagName + '.input.empty'));
this.input.setAttribute('autocorrect', 'off');
this.input.setAttribute('autocapitalize', 'off');
@ -398,7 +398,7 @@ export class InputBox extends Widget {
return !!this.validation && !this.validation(this.value);
}
public validate(): boolean {
public validate(): MessageType | undefined {
let errorMsg: IMessage | null = null;
if (this.validation) {
@ -414,7 +414,7 @@ export class InputBox extends Widget {
}
}
return !errorMsg;
return errorMsg?.type;
}
public stylesForType(type: MessageType | undefined): { border: Color | undefined; background: Color | undefined; foreground: Color | undefined } {

View File

@ -33,7 +33,7 @@
.monaco-list-row {
position: absolute;
box-sizing: border-box;
box-sizing: border-box;
overflow: hidden;
width: 100%;
}
@ -49,7 +49,9 @@
}
/* Focus */
.monaco-list.element-focused, .monaco-list.selection-single, .monaco-list.selection-multiple {
.monaco-list.element-focused,
.monaco-list.selection-single,
.monaco-list.selection-multiple {
outline: 0 !important;
}
@ -64,6 +66,7 @@
border-radius: 10px;
font-size: 12px;
position: absolute;
z-index: 1000;
}
/* Type filter */

View File

@ -432,8 +432,24 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
const deleteRange = { start, end: start + deleteCount };
const removeRange = Range.intersect(previousRenderRange, deleteRange);
// try to reuse rows, avoid removing them from DOM
const rowsToDispose = new Map<string, [IRow, T, number, number][]>();
for (let i = removeRange.start; i < removeRange.end; i++) {
this.removeItemFromDOM(i);
const item = this.items[i];
item.dragStartDisposable.dispose();
if (item.row) {
let rows = rowsToDispose.get(item.templateId);
if (!rows) {
rows = [];
rowsToDispose.set(item.templateId, rows);
}
rows.push([item.row, item.element, i, item.size]);
}
item.row = null;
}
const previousRestRange: IRange = { start: start + deleteCount, end: this.items.length };
@ -491,7 +507,34 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
for (const range of insertRanges) {
for (let i = range.start; i < range.end; i++) {
this.insertItemInDOM(i, beforeElement);
const item = this.items[i];
const rows = rowsToDispose.get(item.templateId);
const rowData = rows?.pop();
if (!rowData) {
this.insertItemInDOM(i, beforeElement);
} else {
const [row, element, index, size] = rowData;
const renderer = this.renderers.get(item.templateId);
if (renderer && renderer.disposeElement) {
renderer.disposeElement(element, index, row.templateData, size);
}
this.insertItemInDOM(i, beforeElement, row);
}
}
}
for (const [templateId, rows] of rowsToDispose) {
for (const [row, element, index, size] of rows) {
const renderer = this.renderers.get(templateId);
if (renderer && renderer.disposeElement) {
renderer.disposeElement(element, index, row.templateData, size);
}
this.cache.release(row);
}
}
@ -699,24 +742,26 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
// DOM operations
private insertItemInDOM(index: number, beforeElement: HTMLElement | null): void {
private insertItemInDOM(index: number, beforeElement: HTMLElement | null, row?: IRow): void {
const item = this.items[index];
if (!item.row) {
item.row = this.cache.alloc(item.templateId);
const role = this.accessibilityProvider.getRole(item.element) || 'listitem';
item.row!.domNode!.setAttribute('role', role);
const checked = this.accessibilityProvider.isChecked(item.element);
if (typeof checked !== 'undefined') {
item.row!.domNode!.setAttribute('aria-checked', String(!!checked));
}
item.row = row ?? this.cache.alloc(item.templateId);
}
if (!item.row.domNode!.parentElement) {
const role = this.accessibilityProvider.getRole(item.element) || 'listitem';
item.row.domNode.setAttribute('role', role);
const checked = this.accessibilityProvider.isChecked(item.element);
if (typeof checked !== 'undefined') {
item.row.domNode.setAttribute('aria-checked', String(!!checked));
}
if (!item.row.domNode.parentElement) {
if (beforeElement) {
this.rowsContainer.insertBefore(item.row.domNode!, beforeElement);
this.rowsContainer.insertBefore(item.row.domNode, beforeElement);
} else {
this.rowsContainer.appendChild(item.row.domNode!);
this.rowsContainer.appendChild(item.row.domNode);
}
}
@ -734,10 +779,10 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
const uri = this.dnd.getDragURI(item.element);
item.dragStartDisposable.dispose();
item.row.domNode!.draggable = !!uri;
item.row.domNode.draggable = !!uri;
if (uri) {
const onDragStart = domEvent(item.row.domNode!, 'dragstart');
const onDragStart = domEvent(item.row.domNode, 'dragstart');
item.dragStartDisposable = onDragStart(event => this.onDragStart(item.element, uri, event));
}
@ -768,36 +813,39 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
}
private updateItemInDOM(item: IItem<T>, index: number): void {
item.row!.domNode!.style.top = `${this.elementTop(index)}px`;
item.row!.domNode.style.top = `${this.elementTop(index)}px`;
if (this.setRowHeight) {
item.row!.domNode!.style.height = `${item.size}px`;
item.row!.domNode.style.height = `${item.size}px`;
}
if (this.setRowLineHeight) {
item.row!.domNode!.style.lineHeight = `${item.size}px`;
item.row!.domNode.style.lineHeight = `${item.size}px`;
}
item.row!.domNode!.setAttribute('data-index', `${index}`);
item.row!.domNode!.setAttribute('data-last-element', index === this.length - 1 ? 'true' : 'false');
item.row!.domNode!.setAttribute('aria-setsize', String(this.accessibilityProvider.getSetSize(item.element, index, this.length)));
item.row!.domNode!.setAttribute('aria-posinset', String(this.accessibilityProvider.getPosInSet(item.element, index)));
item.row!.domNode!.setAttribute('id', this.getElementDomId(index));
item.row!.domNode.setAttribute('data-index', `${index}`);
item.row!.domNode.setAttribute('data-last-element', index === this.length - 1 ? 'true' : 'false');
item.row!.domNode.setAttribute('aria-setsize', String(this.accessibilityProvider.getSetSize(item.element, index, this.length)));
item.row!.domNode.setAttribute('aria-posinset', String(this.accessibilityProvider.getPosInSet(item.element, index)));
item.row!.domNode.setAttribute('id', this.getElementDomId(index));
item.row!.domNode!.classList.toggle('drop-target', item.dropTarget);
item.row!.domNode.classList.toggle('drop-target', item.dropTarget);
}
private removeItemFromDOM(index: number): void {
const item = this.items[index];
item.dragStartDisposable.dispose();
const renderer = this.renderers.get(item.templateId);
if (item.row && renderer && renderer.disposeElement) {
renderer.disposeElement(item.element, index, item.row.templateData, item.size);
}
if (item.row) {
const renderer = this.renderers.get(item.templateId);
this.cache.release(item.row!);
item.row = null;
if (renderer && renderer.disposeElement) {
renderer.disposeElement(item.element, index, item.row.templateData, item.size);
}
this.cache.release(item.row);
item.row = null;
}
if (this.horizontalScrolling) {
this.eventuallyUpdateScrollWidth();
@ -809,14 +857,14 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
return scrollPosition.scrollTop;
}
setScrollTop(scrollTop: number): void {
setScrollTop(scrollTop: number, reuseAnimation?: boolean): void {
if (this.scrollableElementUpdateDisposable) {
this.scrollableElementUpdateDisposable.dispose();
this.scrollableElementUpdateDisposable = null;
this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight });
}
this.scrollableElement.setScrollPosition({ scrollTop });
this.scrollableElement.setScrollPosition({ scrollTop, reuseAnimation });
}
getScrollLeft(): number {
@ -895,9 +943,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
this.render(previousRenderRange, e.scrollTop, e.height, e.scrollLeft, e.scrollWidth);
if (this.supportDynamicHeights) {
// Don't update scrollTop from within an scroll event
// so we don't break smooth scrolling. #104144
this._rerender(e.scrollTop, e.height, false);
this._rerender(e.scrollTop, e.height, e.inSmoothScrolling);
}
} catch (err) {
console.error('Got bad scroll event:', e);
@ -1027,7 +1073,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
const item = this.items[index]!;
item.dropTarget = true;
if (item.row && item.row.domNode) {
if (item.row) {
item.row.domNode.classList.add('drop-target');
}
}
@ -1037,7 +1083,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
const item = this.items[index]!;
item.dropTarget = false;
if (item.row && item.row.domNode) {
if (item.row) {
item.row.domNode.classList.remove('drop-target');
}
}
@ -1167,7 +1213,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
* Given a stable rendered state, checks every rendered element whether it needs
* to be probed for dynamic height. Adjusts scroll height and top if necessary.
*/
private _rerender(renderTop: number, renderHeight: number, updateScrollTop: boolean = true): void {
private _rerender(renderTop: number, renderHeight: number, inSmoothScrolling?: boolean): void {
const previousRenderRange = this.getRenderRange(renderTop, renderHeight);
// Let's remember the second element's position, this helps in scrolling up
@ -1233,8 +1279,15 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
}
}
if (updateScrollTop && typeof anchorElementIndex === 'number') {
this.scrollTop = this.elementTop(anchorElementIndex) - anchorElementTopDelta!;
if (typeof anchorElementIndex === 'number') {
// To compute a destination scroll top, we need to take into account the current smooth scrolling
// animation, and then reuse it with a new target (to avoid prolonging the scroll)
// See https://github.com/microsoft/vscode/issues/104144
// See https://github.com/microsoft/vscode/pull/104284
// See https://github.com/microsoft/vscode/issues/107704
const deltaScrollTop = this.scrollable.getFutureScrollPosition().scrollTop - renderTop;
const newScrollTop = this.elementTop(anchorElementIndex) - anchorElementTopDelta! + deltaScrollTop;
this.setScrollTop(newScrollTop, inSmoothScrolling);
}
this._onDidChangeContentHeight.fire(this.contentHeight);
@ -1256,7 +1309,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
const size = item.size;
if (!this.setRowHeight && item.row && item.row.domNode) {
if (!this.setRowHeight && item.row) {
let newSize = item.row.domNode.offsetHeight;
item.size = newSize;
item.lastDynamicHeightWidth = this.renderWidth;
@ -1265,8 +1318,8 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
const row = this.cache.alloc(item.templateId);
row.domNode!.style.height = '';
this.rowsContainer.appendChild(row.domNode!);
row.domNode.style.height = '';
this.rowsContainer.appendChild(row.domNode);
const renderer = this.renderers.get(item.templateId);
if (renderer) {
@ -1277,14 +1330,14 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
}
}
item.size = row.domNode!.offsetHeight;
item.size = row.domNode.offsetHeight;
if (this.virtualDelegate.setDynamicHeight) {
this.virtualDelegate.setDynamicHeight(item.element, item.size);
}
item.lastDynamicHeightWidth = this.renderWidth;
this.rowsContainer.removeChild(row.domNode!);
this.rowsContainer.removeChild(row.domNode);
this.cache.release(row);
return item.size - size;

View File

@ -8,7 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle';
import { $ } from 'vs/base/browser/dom';
export interface IRow {
domNode: HTMLElement | null;
domNode: HTMLElement;
templateId: string;
templateData: any;
}
@ -84,7 +84,6 @@ export class RowCache<T> implements IDisposable {
for (const cachedRow of cachedRows) {
const renderer = this.getRenderer(templateId);
renderer.disposeTemplate(cachedRow.templateData);
cachedRow.domNode = null;
cachedRow.templateData = null;
}
});

View File

@ -18,11 +18,12 @@ import { ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable';
import { Event } from 'vs/base/common/event';
import { AnchorAlignment, layout, LayoutAnchorPosition } from 'vs/base/browser/ui/contextview/contextview';
import { isLinux, isMacintosh } from 'vs/base/common/platform';
import { Codicon, registerCodicon, stripCodicons } from 'vs/base/common/codicons';
import { Codicon, registerCodicon } from 'vs/base/common/codicons';
import { BaseActionViewItem, ActionViewItem, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { formatRule } from 'vs/base/browser/ui/codicons/codiconStyles';
import { isFirefox } from 'vs/base/browser/browser';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { stripIcons } from 'vs/base/common/iconLabels';
export const MENU_MNEMONIC_REGEX = /\(&([^\s&])\)|(^|[^&])&([^\s&])/;
export const MENU_ESCAPED_MNEMONIC_REGEX = /(&amp;)?(&amp;)([^\s&])/g;
@ -532,7 +533,7 @@ class BaseMenuActionViewItem extends BaseActionViewItem {
if (this.options.label) {
clearNode(this.label);
let label = stripCodicons(this.getAction().label);
let label = stripIcons(this.getAction().label);
if (label) {
const cleanLabel = cleanMnemonic(label);
if (!this.options.enableMnemonics) {

View File

@ -126,9 +126,11 @@ export class MenuBar extends Disposable {
let eventHandled = true;
const key = !!e.key ? e.key.toLocaleLowerCase() : '';
if (event.equals(KeyCode.LeftArrow) || (isMacintosh && event.equals(KeyCode.Tab | KeyMod.Shift))) {
const tabNav = isMacintosh && this.options.compactMode === undefined;
if (event.equals(KeyCode.LeftArrow) || (tabNav && event.equals(KeyCode.Tab | KeyMod.Shift))) {
this.focusPrevious();
} else if (event.equals(KeyCode.RightArrow) || (isMacintosh && event.equals(KeyCode.Tab))) {
} else if (event.equals(KeyCode.RightArrow) || (tabNav && event.equals(KeyCode.Tab))) {
this.focusNext();
} else if (event.equals(KeyCode.Escape) && this.isFocused && !this.isOpen) {
this.setUnfocusedState();

View File

@ -83,7 +83,7 @@ export abstract class AbstractScrollbar extends Widget {
* Creates the dom node for an arrow & adds it to the container
*/
protected _createArrow(opts: ScrollbarArrowOptions): void {
let arrow = this._register(new ScrollbarArrow(opts));
const arrow = this._register(new ScrollbarArrow(opts));
this.domNode.domNode.appendChild(arrow.bgDomNode);
this.domNode.domNode.appendChild(arrow.domNode);
}
@ -186,10 +186,10 @@ export abstract class AbstractScrollbar extends Widget {
}
public delegateMouseDown(e: IMouseEvent): void {
let domTop = this.domNode.domNode.getClientRects()[0].top;
let sliderStart = domTop + this._scrollbarState.getSliderPosition();
let sliderStop = domTop + this._scrollbarState.getSliderPosition() + this._scrollbarState.getSliderSize();
let mousePos = this._sliderMousePosition(e);
const domTop = this.domNode.domNode.getClientRects()[0].top;
const sliderStart = domTop + this._scrollbarState.getSliderPosition();
const sliderStop = domTop + this._scrollbarState.getSliderPosition() + this._scrollbarState.getSliderSize();
const mousePos = this._sliderMousePosition(e);
if (sliderStart <= mousePos && mousePos <= sliderStop) {
// Act as if it was a mouse down on the slider
if (e.leftButton) {
@ -263,7 +263,7 @@ export abstract class AbstractScrollbar extends Widget {
private _setDesiredScrollPositionNow(_desiredScrollPosition: number): void {
let desiredScrollPosition: INewScrollPosition = {};
const desiredScrollPosition: INewScrollPosition = {};
this.writeScrollPosition(desiredScrollPosition, _desiredScrollPosition);
this._scrollable.setScrollPositionNow(desiredScrollPosition);
@ -278,6 +278,10 @@ export abstract class AbstractScrollbar extends Widget {
}
}
public isNeeded(): boolean {
return this._scrollbarState.isNeeded();
}
// ----------------- Overwrite these
protected abstract _renderDomNode(largeSize: number, smallSize: number): void;

View File

@ -38,8 +38,8 @@ export class HorizontalScrollbar extends AbstractScrollbar {
});
if (options.horizontalHasArrows) {
let arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2;
let scrollbarDelta = (options.horizontalScrollbarSize - ARROW_IMG_SIZE) / 2;
const arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2;
const scrollbarDelta = (options.horizontalScrollbarSize - ARROW_IMG_SIZE) / 2;
this._createArrow({
className: 'scra',

View File

@ -187,7 +187,7 @@ export abstract class AbstractScrollableElement extends Widget {
this._onScroll.fire(e);
}));
let scrollbarHost: ScrollbarHost = {
const scrollbarHost: ScrollbarHost = {
onMouseWheel: (mouseWheelEvent: StandardWheelEvent) => this._onMouseWheel(mouseWheelEvent),
onDragStart: () => this._onDragStart(),
onDragEnd: () => this._onDragEnd(),
@ -325,7 +325,7 @@ export abstract class AbstractScrollableElement extends Widget {
// -------------------- mouse wheel scrolling --------------------
private _setListeningToMouseWheel(shouldListen: boolean): void {
let isListening = (this._mouseWheelToDispose.length > 0);
const isListening = (this._mouseWheelToDispose.length > 0);
if (isListening === shouldListen) {
// No change
@ -337,7 +337,7 @@ export abstract class AbstractScrollableElement extends Widget {
// Start listening (if necessary)
if (shouldListen) {
let onMouseWheel = (browserEvent: IMouseWheelEvent) => {
const onMouseWheel = (browserEvent: IMouseWheelEvent) => {
this._onMouseWheel(new StandardWheelEvent(browserEvent));
};
@ -426,7 +426,15 @@ export abstract class AbstractScrollableElement extends Widget {
}
}
if (this._options.alwaysConsumeMouseWheel || didScroll) {
let consumeMouseWheel = didScroll;
if (!consumeMouseWheel && this._options.alwaysConsumeMouseWheel) {
consumeMouseWheel = true;
}
if (!consumeMouseWheel && this._options.consumeMouseWheelIfScrollbarIsNeeded && (this._verticalScrollbar.isNeeded() || this._horizontalScrollbar.isNeeded())) {
consumeMouseWheel = true;
}
if (consumeMouseWheel) {
e.preventDefault();
e.stopPropagation();
}
@ -473,8 +481,8 @@ export abstract class AbstractScrollableElement extends Widget {
if (this._options.useShadows) {
const scrollState = this._scrollable.getCurrentScrollPosition();
let enableTop = scrollState.scrollTop > 0;
let enableLeft = scrollState.scrollLeft > 0;
const enableTop = scrollState.scrollTop > 0;
const enableLeft = scrollState.scrollLeft > 0;
this._leftShadowDomNode!.setClassName('shadow' + (enableLeft ? ' left' : ''));
this._topShadowDomNode!.setClassName('shadow' + (enableTop ? ' top' : ''));
@ -549,8 +557,12 @@ export class SmoothScrollableElement extends AbstractScrollableElement {
super(element, options, scrollable);
}
public setScrollPosition(update: INewScrollPosition): void {
this._scrollable.setScrollPositionNow(update);
public setScrollPosition(update: INewScrollPosition & { reuseAnimation?: boolean }): void {
if (update.reuseAnimation) {
this._scrollable.setScrollPositionSmooth(update, update.reuseAnimation);
} else {
this._scrollable.setScrollPositionNow(update);
}
}
public getScrollPosition(): IScrollPosition {
@ -593,12 +605,13 @@ export class DomScrollableElement extends ScrollableElement {
}
function resolveOptions(opts: ScrollableElementCreationOptions): ScrollableElementResolvedOptions {
let result: ScrollableElementResolvedOptions = {
const result: ScrollableElementResolvedOptions = {
lazyRender: (typeof opts.lazyRender !== 'undefined' ? opts.lazyRender : false),
className: (typeof opts.className !== 'undefined' ? opts.className : ''),
useShadows: (typeof opts.useShadows !== 'undefined' ? opts.useShadows : true),
handleMouseWheel: (typeof opts.handleMouseWheel !== 'undefined' ? opts.handleMouseWheel : true),
flipAxes: (typeof opts.flipAxes !== 'undefined' ? opts.flipAxes : false),
consumeMouseWheelIfScrollbarIsNeeded: (typeof opts.consumeMouseWheelIfScrollbarIsNeeded !== 'undefined' ? opts.consumeMouseWheelIfScrollbarIsNeeded : false),
alwaysConsumeMouseWheel: (typeof opts.alwaysConsumeMouseWheel !== 'undefined' ? opts.alwaysConsumeMouseWheel : false),
scrollYToX: (typeof opts.scrollYToX !== 'undefined' ? opts.scrollYToX : false),
mouseWheelScrollSensitivity: (typeof opts.mouseWheelScrollSensitivity !== 'undefined' ? opts.mouseWheelScrollSensitivity : 1),

View File

@ -40,6 +40,11 @@ export interface ScrollableElementCreationOptions {
* Defaults to false.
*/
scrollYToX?: boolean;
/**
* Consume all mouse wheel events if a scrollbar is needed (i.e. scrollSize > size).
* Defaults to false.
*/
consumeMouseWheelIfScrollbarIsNeeded?: boolean;
/**
* Always consume mouse wheel events, even when scrolling is no longer possible.
* Defaults to false.
@ -136,6 +141,7 @@ export interface ScrollableElementResolvedOptions {
handleMouseWheel: boolean;
flipAxes: boolean;
scrollYToX: boolean;
consumeMouseWheelIfScrollbarIsNeeded: boolean;
alwaysConsumeMouseWheel: boolean;
mouseWheelScrollSensitivity: number;
fastScrollSensitivity: number;

View File

@ -88,7 +88,7 @@ export class ScrollbarArrow extends Widget {
}
private _arrowMouseDown(e: IMouseEvent): void {
let scheduleRepeater = () => {
const scheduleRepeater = () => {
this._mousedownRepeatTimer.cancelAndSet(() => this._onActivate(), 1000 / 24);
};

View File

@ -85,7 +85,7 @@ export class ScrollbarState {
}
public setVisibleSize(visibleSize: number): boolean {
let iVisibleSize = Math.round(visibleSize);
const iVisibleSize = Math.round(visibleSize);
if (this._visibleSize !== iVisibleSize) {
this._visibleSize = iVisibleSize;
this._refreshComputedValues();
@ -95,7 +95,7 @@ export class ScrollbarState {
}
public setScrollSize(scrollSize: number): boolean {
let iScrollSize = Math.round(scrollSize);
const iScrollSize = Math.round(scrollSize);
if (this._scrollSize !== iScrollSize) {
this._scrollSize = iScrollSize;
this._refreshComputedValues();
@ -105,7 +105,7 @@ export class ScrollbarState {
}
public setScrollPosition(scrollPosition: number): boolean {
let iScrollPosition = Math.round(scrollPosition);
const iScrollPosition = Math.round(scrollPosition);
if (this._scrollPosition !== iScrollPosition) {
this._scrollPosition = iScrollPosition;
this._refreshComputedValues();
@ -198,7 +198,7 @@ export class ScrollbarState {
return 0;
}
let desiredSliderPosition = offset - this._arrowSize - this._computedSliderSize / 2;
const desiredSliderPosition = offset - this._arrowSize - this._computedSliderSize / 2;
return Math.round(desiredSliderPosition / this._computedSliderRatio);
}
@ -214,7 +214,7 @@ export class ScrollbarState {
return 0;
}
let correctedOffset = offset - this._arrowSize; // compensate if has arrows
const correctedOffset = offset - this._arrowSize; // compensate if has arrows
let desiredScrollPosition = this._scrollPosition;
if (correctedOffset < this._computedSliderPosition) {
desiredScrollPosition -= this._visibleSize; // page up/left
@ -233,7 +233,7 @@ export class ScrollbarState {
return 0;
}
let desiredSliderPosition = this._computedSliderPosition + delta;
const desiredSliderPosition = this._computedSliderPosition + delta;
return Math.round(desiredSliderPosition / this._computedSliderRatio);
}
}

View File

@ -43,7 +43,7 @@ export class ScrollbarVisibilityController extends Disposable {
}
public setShouldBeVisible(rawShouldBeVisible: boolean): void {
let shouldBeVisible = this.applyVisibilitySetting(rawShouldBeVisible);
const shouldBeVisible = this.applyVisibilitySetting(rawShouldBeVisible);
if (this._shouldBeVisible !== shouldBeVisible) {
this._shouldBeVisible = shouldBeVisible;
@ -105,4 +105,4 @@ export class ScrollbarVisibilityController extends Disposable {
this._domNode.setClassName(this._invisibleClassName + (withFadeAway ? ' fade' : ''));
}
}
}
}

View File

@ -38,8 +38,8 @@ export class VerticalScrollbar extends AbstractScrollbar {
});
if (options.verticalHasArrows) {
let arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2;
let scrollbarDelta = (options.verticalScrollbarSize - ARROW_IMG_SIZE) / 2;
const arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2;
const scrollbarDelta = (options.verticalScrollbarSize - ARROW_IMG_SIZE) / 2;
this._createArrow({
className: 'scra',

View File

@ -73,7 +73,7 @@ export class ToolBar extends Disposable {
actionViewItemProvider: this.options.actionViewItemProvider,
actionRunner: this.actionRunner,
keybindingProvider: this.options.getKeyBinding,
classNames: (options.moreIcon ?? toolBarMoreIcon).classNames,
classNames: CSSIcon.asClassNameArray(options.moreIcon ?? toolBarMoreIcon),
anchorAlignmentProvider: this.options.anchorAlignmentProvider,
menuAsChild: !!this.options.renderDropdownAsChildElement
}

View File

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { ComposedTreeDelegate, IAbstractTreeOptions, IAbstractTreeOptionsUpdate } from 'vs/base/browser/ui/tree/abstractTree';
import { ObjectTree, IObjectTreeOptions, CompressibleObjectTree, ICompressibleTreeRenderer, ICompressibleKeyboardNavigationLabelProvider, ICompressibleObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree';
import { ObjectTree, IObjectTreeOptions, CompressibleObjectTree, ICompressibleTreeRenderer, ICompressibleKeyboardNavigationLabelProvider, ICompressibleObjectTreeOptions, IObjectTreeSetChildrenOptions } from 'vs/base/browser/ui/tree/objectTree';
import { IListVirtualDelegate, IIdentityProvider, IListDragAndDrop, IListDragOverReaction } from 'vs/base/browser/ui/list/list';
import { ITreeElement, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeSorter, ICollapseStateChangeEvent, IAsyncDataSource, ITreeDragAndDrop, TreeError, WeakMapper, ITreeFilter, TreeVisibility, TreeFilterResult } from 'vs/base/browser/ui/tree/tree';
import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
@ -279,6 +279,7 @@ function asObjectTreeOptions<TInput, T, TFilterData>(options?: IAsyncDataTreeOpt
}
export interface IAsyncDataTreeOptionsUpdate extends IAbstractTreeOptionsUpdate { }
export interface IAsyncDataTreeUpdateChildrenOptions<T> extends IObjectTreeSetChildrenOptions<T> { }
export interface IAsyncDataTreeOptions<T, TFilterData = void> extends IAsyncDataTreeOptionsUpdate, Pick<IAbstractTreeOptions<T, TFilterData>, Exclude<keyof IAbstractTreeOptions<T, TFilterData>, 'collapseByDefault'>> {
readonly collapseByDefault?: { (e: T): boolean; };
@ -499,11 +500,11 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
}
}
async updateChildren(element: TInput | T = this.root.element, recursive = true, rerender = false): Promise<void> {
await this._updateChildren(element, recursive, rerender);
async updateChildren(element: TInput | T = this.root.element, recursive = true, rerender = false, options?: IAsyncDataTreeUpdateChildrenOptions<T>): Promise<void> {
await this._updateChildren(element, recursive, rerender, undefined, options);
}
private async _updateChildren(element: TInput | T = this.root.element, recursive = true, rerender = false, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
private async _updateChildren(element: TInput | T = this.root.element, recursive = true, rerender = false, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>, options?: IAsyncDataTreeUpdateChildrenOptions<T>): Promise<void> {
if (typeof this.root.element === 'undefined') {
throw new TreeError(this.user, 'Tree input not set');
}
@ -514,7 +515,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
}
const node = this.getDataNode(element);
await this.refreshAndRenderNode(node, recursive, viewStateContext);
await this.refreshAndRenderNode(node, recursive, viewStateContext, options);
if (rerender) {
try {
@ -704,9 +705,9 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
return node;
}
private async refreshAndRenderNode(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
private async refreshAndRenderNode(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>, options?: IAsyncDataTreeUpdateChildrenOptions<T>): Promise<void> {
await this.refreshNode(node, recursive, viewStateContext);
this.render(node, viewStateContext);
this.render(node, viewStateContext, options);
}
private async refreshNode(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
@ -768,8 +769,8 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
const children = await childrenPromise;
return this.setChildren(node, children, recursive, viewStateContext);
} catch (err) {
if (node !== this.root) {
this.tree.collapse(node === this.root ? null : node);
if (node !== this.root && this.tree.hasElement(node)) {
this.tree.collapse(node);
}
if (isPromiseCanceledError(err)) {
@ -921,9 +922,18 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
return childrenToRefresh;
}
protected render(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): void {
protected render(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>, options?: IAsyncDataTreeUpdateChildrenOptions<T>): void {
const children = node.children.map(node => this.asTreeElement(node, viewStateContext));
this.tree.setChildren(node === this.root ? null : node, children);
const objectTreeOptions: IObjectTreeSetChildrenOptions<IAsyncDataTreeNode<TInput, T>> | undefined = options && {
...options,
diffIdentityProvider: options!.diffIdentityProvider && {
getId(node: IAsyncDataTreeNode<TInput, T>): { toString(): string; } {
return options!.diffIdentityProvider!.getId(node.element as T);
}
}
};
this.tree.setChildren(node === this.root ? null : node, children, objectTreeOptions);
if (node !== this.root) {
this.tree.setCollapsible(node, node.hasChildren);

View File

@ -6,8 +6,9 @@
import { Iterable } from 'vs/base/common/iterator';
import { Event } from 'vs/base/common/event';
import { ITreeModel, ITreeNode, ITreeElement, ICollapseStateChangeEvent, ITreeModelSpliceEvent, TreeError, TreeFilterResult, TreeVisibility, WeakMapper } from 'vs/base/browser/ui/tree/tree';
import { IObjectTreeModelOptions, ObjectTreeModel, IObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel';
import { IList } from 'vs/base/browser/ui/tree/indexTreeModel';
import { IObjectTreeModelOptions, ObjectTreeModel, IObjectTreeModel, IObjectTreeModelSetChildrenOptions } from 'vs/base/browser/ui/tree/objectTreeModel';
import { IIndexTreeModelSpliceOptions, IList } from 'vs/base/browser/ui/tree/indexTreeModel';
import { IIdentityProvider } from 'vs/base/browser/ui/list/list';
// Exported only for test reasons, do not use directly
export interface ICompressedTreeElement<T> extends ITreeElement<T> {
@ -108,6 +109,12 @@ interface ICompressedObjectTreeModelOptions<T, TFilterData> extends IObjectTreeM
readonly compressionEnabled?: boolean;
}
const wrapIdentityProvider = <T>(base: IIdentityProvider<T>): IIdentityProvider<ICompressedTreeNode<T>> => ({
getId(node) {
return node.elements.map(e => base.getId(e).toString()).join('\0');
}
});
// Exported only for test reasons, do not use directly
export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData extends NonNullable<any> = void> implements ITreeModel<ICompressedTreeNode<T> | null, TFilterData, T | null> {
@ -120,6 +127,7 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
private model: ObjectTreeModel<ICompressedTreeNode<T>, TFilterData>;
private nodes = new Map<T | null, ICompressedTreeNode<T>>();
private enabled: boolean;
private readonly identityProvider?: IIdentityProvider<ICompressedTreeNode<T>>;
get size(): number { return this.nodes.size; }
@ -130,15 +138,21 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
) {
this.model = new ObjectTreeModel(user, list, options);
this.enabled = typeof options.compressionEnabled === 'undefined' ? true : options.compressionEnabled;
this.identityProvider = options.identityProvider;
}
setChildren(
element: T | null,
children: Iterable<ICompressedTreeElement<T>> = Iterable.empty()
children: Iterable<ICompressedTreeElement<T>> = Iterable.empty(),
options: IObjectTreeModelSetChildrenOptions<T, TFilterData>,
): void {
// Diffs must be deem, since the compression can affect nested elements.
// @see https://github.com/microsoft/vscode/pull/114237#issuecomment-759425034
const diffIdentityProvider = options.diffIdentityProvider && wrapIdentityProvider(options.diffIdentityProvider);
if (element === null) {
const compressedChildren = Iterable.map(children, this.enabled ? compress : noCompress);
this._setChildren(null, compressedChildren);
this._setChildren(null, compressedChildren, { diffIdentityProvider, diffDepth: Infinity });
return;
}
@ -159,7 +173,10 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
const parentChildren = parent.children
.map(child => child === node ? recompressedElement : child);
this._setChildren(parent.element, parentChildren);
this._setChildren(parent.element, parentChildren, {
diffIdentityProvider,
diffDepth: node.depth - parent.depth,
});
}
isCompressionEnabled(): boolean {
@ -177,22 +194,29 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
const rootChildren = root.children as ITreeNode<ICompressedTreeNode<T>>[];
const decompressedRootChildren = Iterable.map(rootChildren, decompress);
const recompressedRootChildren = Iterable.map(decompressedRootChildren, enabled ? compress : noCompress);
this._setChildren(null, recompressedRootChildren);
// it should be safe to always use deep diff mode here if an identity
// provider is available, since we know the raw nodes are unchanged.
this._setChildren(null, recompressedRootChildren, {
diffIdentityProvider: this.identityProvider,
diffDepth: Infinity,
});
}
private _setChildren(
node: ICompressedTreeNode<T> | null,
children: Iterable<ITreeElement<ICompressedTreeNode<T>>>
children: Iterable<ITreeElement<ICompressedTreeNode<T>>>,
options: IIndexTreeModelSpliceOptions<ICompressedTreeNode<T>, TFilterData>,
): void {
const insertedElements = new Set<T | null>();
const _onDidCreateNode = (node: ITreeNode<ICompressedTreeNode<T>, TFilterData>) => {
const onDidCreateNode = (node: ITreeNode<ICompressedTreeNode<T>, TFilterData>) => {
for (const element of node.element.elements) {
insertedElements.add(element);
this.nodes.set(element, node.element);
}
};
const _onDidDeleteNode = (node: ITreeNode<ICompressedTreeNode<T>, TFilterData>) => {
const onDidDeleteNode = (node: ITreeNode<ICompressedTreeNode<T>, TFilterData>) => {
for (const element of node.element.elements) {
if (!insertedElements.has(element)) {
this.nodes.delete(element);
@ -200,7 +224,7 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
}
};
this.model.setChildren(node, children, _onDidCreateNode, _onDidDeleteNode);
this.model.setChildren(node, children, { ...options, onDidCreateNode, onDidDeleteNode });
}
has(element: T | null): boolean {
@ -363,16 +387,16 @@ function mapList<T, TFilterData>(nodeMapper: CompressedNodeWeakMapper<T, TFilter
function mapOptions<T, TFilterData>(compressedNodeUnwrapper: CompressedNodeUnwrapper<T>, options: ICompressibleObjectTreeModelOptions<T, TFilterData>): ICompressedObjectTreeModelOptions<T, TFilterData> {
return {
...options,
sorter: options.sorter && {
compare(node: ICompressedTreeNode<T>, otherNode: ICompressedTreeNode<T>): number {
return options.sorter!.compare(node.elements[0], otherNode.elements[0]);
}
},
identityProvider: options.identityProvider && {
getId(node: ICompressedTreeNode<T>): { toString(): string; } {
return options.identityProvider!.getId(compressedNodeUnwrapper(node));
}
},
sorter: options.sorter && {
compare(node: ICompressedTreeNode<T>, otherNode: ICompressedTreeNode<T>): number {
return options.sorter!.compare(node.elements[0], otherNode.elements[0]);
}
},
filter: options.filter && {
filter(node: ICompressedTreeNode<T>, parentVisibility: TreeVisibility): TreeFilterResult<TFilterData> {
return options.filter!.filter(compressedNodeUnwrapper(node), parentVisibility);
@ -424,8 +448,12 @@ export class CompressibleObjectTreeModel<T extends NonNullable<any>, TFilterData
this.model = new CompressedObjectTreeModel(user, mapList(this.nodeMapper, list), mapOptions(compressedNodeUnwrapper, options));
}
setChildren(element: T | null, children: Iterable<ICompressedTreeElement<T>> = Iterable.empty()): void {
this.model.setChildren(element, children);
setChildren(
element: T | null,
children: Iterable<ICompressedTreeElement<T>> = Iterable.empty(),
options: IObjectTreeModelSetChildrenOptions<T, TFilterData> = {},
): void {
this.model.setChildren(element, children, options);
}
isCompressionEnabled(): boolean {

View File

@ -47,13 +47,19 @@ export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | nu
return this.input;
}
setInput(input: TInput, viewState?: IDataTreeViewState): void {
setInput(input: TInput | undefined, viewState?: IDataTreeViewState): void {
if (viewState && !this.identityProvider) {
throw new TreeError(this.user, 'Can\'t restore tree view state without an identity provider');
}
this.input = input;
if (!input) {
this.nodesByIdentity.clear();
this.model.setChildren(null, Iterable.empty());
return;
}
if (!viewState) {
this._refresh(input);
return;
@ -155,7 +161,7 @@ export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | nu
};
}
this.model.setChildren((element === this.input ? null : element) as T, this.iterate(element, isCollapsed).elements, onDidCreateNode, onDidDeleteNode);
this.model.setChildren((element === this.input ? null : element) as T, this.iterate(element, isCollapsed).elements, { onDidCreateNode, onDidDeleteNode });
}
private iterate(element: TInput | T, isCollapsed?: (el: T) => boolean | undefined): { elements: Iterable<ITreeElement<T>>, size: number } {

View File

@ -3,8 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IIdentityProvider } from 'vs/base/browser/ui/list/list';
import { ICollapseStateChangeEvent, ITreeElement, ITreeFilter, ITreeFilterDataResult, ITreeModel, ITreeNode, TreeVisibility, ITreeModelSpliceEvent, TreeError } from 'vs/base/browser/ui/tree/tree';
import { tail2 } from 'vs/base/common/arrays';
import { LcsDiff } from 'vs/base/common/diff/diff';
import { Emitter, Event, EventBufferer } from 'vs/base/common/event';
import { Iterable } from 'vs/base/common/iterator';
import { ISpliceable } from 'vs/base/common/sequence';
@ -41,6 +43,34 @@ export interface IIndexTreeModelOptions<T, TFilterData> {
readonly autoExpandSingleChildren?: boolean;
}
export interface IIndexTreeModelSpliceOptions<T, TFilterData> {
/**
* If set, child updates will recurse the given number of levels even if
* items in the splice operation are unchanged. `Infinity` is a valid value.
*/
readonly diffDepth?: number;
/**
* Identity provider used to optimize splice() calls in the IndexTree. If
* this is not present, optimized splicing is not enabled.
*
* Warning: if this is present, calls to `setChildren()` will not replace
* or update nodes if their identity is the same, even if the elements are
* different. For this, you should call `rerender()`.
*/
readonly diffIdentityProvider?: IIdentityProvider<T>;
/**
* Callback for when a node is created.
*/
onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void;
/**
* Callback for when a node is deleted.
*/
onDidDeleteNode?: (node: ITreeNode<T, TFilterData>) => void
}
interface CollapsibleStateUpdate {
readonly collapsible: boolean;
}
@ -110,18 +140,95 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
location: number[],
deleteCount: number,
toInsert: Iterable<ITreeElement<T>> = Iterable.empty(),
onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void,
onDidDeleteNode?: (node: ITreeNode<T, TFilterData>) => void
options: IIndexTreeModelSpliceOptions<T, TFilterData> = {},
): void {
if (location.length === 0) {
throw new TreeError(this.user, 'Invalid tree location');
}
if (options.diffIdentityProvider) {
this.spliceSmart(options.diffIdentityProvider, location, deleteCount, toInsert, options);
} else {
this.spliceSimple(location, deleteCount, toInsert, options);
}
}
private spliceSmart(
identity: IIdentityProvider<T>,
location: number[],
deleteCount: number,
toInsertIterable: Iterable<ITreeElement<T>> = Iterable.empty(),
options: IIndexTreeModelSpliceOptions<T, TFilterData>,
recurseLevels = options.diffDepth ?? 0,
) {
const { parentNode } = this.getParentNodeWithListIndex(location);
const toInsert = [...toInsertIterable];
const index = location[location.length - 1];
const diff = new LcsDiff(
{ getElements: () => parentNode.children.map(e => identity.getId(e.element).toString()) },
{
getElements: () => [
...parentNode.children.slice(0, index),
...toInsert,
...parentNode.children.slice(index + deleteCount),
].map(e => identity.getId(e.element).toString())
},
).ComputeDiff(false);
// if we were given a 'best effort' diff, use default behavior
if (diff.quitEarly) {
return this.spliceSimple(location, deleteCount, toInsert, options);
}
const locationPrefix = location.slice(0, -1);
const recurseSplice = (fromOriginal: number, fromModified: number, count: number) => {
if (recurseLevels > 0) {
for (let i = 0; i < count; i++) {
fromOriginal--;
fromModified--;
this.spliceSmart(
identity,
[...locationPrefix, fromOriginal, 0],
Number.MAX_SAFE_INTEGER,
toInsert[fromModified].children,
options,
recurseLevels - 1,
);
}
}
};
let lastStartO = Math.min(parentNode.children.length, index + deleteCount);
let lastStartM = toInsert.length;
for (const change of diff.changes.sort((a, b) => b.originalStart - a.originalStart)) {
recurseSplice(lastStartO, lastStartM, lastStartO - (change.originalStart + change.originalLength));
lastStartO = change.originalStart;
lastStartM = change.modifiedStart - index;
this.spliceSimple(
[...locationPrefix, lastStartO],
change.originalLength,
Iterable.slice(toInsert, lastStartM, lastStartM + change.modifiedLength),
options,
);
}
// at this point, startO === startM === count since any remaining prefix should match
recurseSplice(lastStartO, lastStartM, lastStartO);
}
private spliceSimple(
location: number[],
deleteCount: number,
toInsert: Iterable<ITreeElement<T>> = Iterable.empty(),
{ onDidCreateNode, onDidDeleteNode }: IIndexTreeModelSpliceOptions<T, TFilterData>,
) {
const { parentNode, listIndex, revealed, visible } = this.getParentNodeWithListIndex(location);
const treeListElementsToInsert: ITreeNode<T, TFilterData>[] = [];
const nodesToInsertIterator = Iterable.map(toInsert, el => this.createTreeNode(el, parentNode, parentNode.visible ? TreeVisibility.Visible : TreeVisibility.Hidden, revealed, treeListElementsToInsert, onDidCreateNode));
const lastIndex = location[location.length - 1];
const lastHadChildren = parentNode.children.length > 0;
// figure out what's the visible child start index right before the
// splice point
@ -190,6 +297,11 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
deletedNodes.forEach(visit);
}
const currentlyHasChildren = parentNode.children.length > 0;
if (lastHadChildren !== currentlyHasChildren) {
this.setCollapsible(location.slice(0, -1), currentlyHasChildren);
}
this._onDidSplice.fire({ insertedNodes: nodesToInsert, deletedNodes });
let node: IIndexTreeNode<T, TFilterData> | undefined = parentNode;

View File

@ -7,7 +7,7 @@ import { Iterable } from 'vs/base/common/iterator';
import { AbstractTree, IAbstractTreeOptions, IAbstractTreeOptionsUpdate } from 'vs/base/browser/ui/tree/abstractTree';
import { ITreeNode, ITreeModel, ITreeElement, ITreeRenderer, ITreeSorter, ICollapseStateChangeEvent } from 'vs/base/browser/ui/tree/tree';
import { ObjectTreeModel, IObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel';
import { IListVirtualDelegate, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list';
import { IListVirtualDelegate, IKeyboardNavigationLabelProvider, IIdentityProvider } from 'vs/base/browser/ui/list/list';
import { Event } from 'vs/base/common/event';
import { CompressibleObjectTreeModel, ElementMapper, ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
import { memoize } from 'vs/base/common/decorators';
@ -17,6 +17,25 @@ export interface IObjectTreeOptions<T, TFilterData = void> extends IAbstractTree
readonly sorter?: ITreeSorter<T>;
}
export interface IObjectTreeSetChildrenOptions<T> {
/**
* If set, child updates will recurse the given number of levels even if
* items in the splice operation are unchanged. `Infinity` is a valid value.
*/
readonly diffDepth?: number;
/**
* Identity provider used to optimize splice() calls in the IndexTree. If
* this is not present, optimized splicing is not enabled.
*
* Warning: if this is present, calls to `setChildren()` will not replace
* or update nodes if their identity is the same, even if the elements are
* different. For this, you should call `rerender()`.
*/
readonly diffIdentityProvider?: IIdentityProvider<T>;
}
export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends AbstractTree<T | null, TFilterData, T | null> {
protected model!: IObjectTreeModel<T, TFilterData>;
@ -33,8 +52,8 @@ export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends
super(user, container, delegate, renderers, options as IObjectTreeOptions<T | null, TFilterData>);
}
setChildren(element: T | null, children: Iterable<ITreeElement<T>> = Iterable.empty()): void {
this.model.setChildren(element, children);
setChildren(element: T | null, children: Iterable<ITreeElement<T>> = Iterable.empty(), options?: IObjectTreeSetChildrenOptions<T>): void {
this.model.setChildren(element, children, options);
}
rerender(element?: T): void {
@ -189,8 +208,8 @@ export class CompressibleObjectTree<T extends NonNullable<any>, TFilterData = vo
super(user, container, delegate, compressibleRenderers, asObjectTreeOptions<T, TFilterData>(compressedTreeNodeProvider, options));
}
setChildren(element: T | null, children: Iterable<ICompressedTreeElement<T>> = Iterable.empty()): void {
this.model.setChildren(element, children);
setChildren(element: T | null, children: Iterable<ICompressedTreeElement<T>> = Iterable.empty(), options?: IObjectTreeSetChildrenOptions<T>): void {
this.model.setChildren(element, children, options);
}
protected createModel(user: string, view: IList<ITreeNode<T, TFilterData>>, options: ICompressibleObjectTreeOptions<T, TFilterData>): ITreeModel<T | null, TFilterData, T | null> {

View File

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Iterable } from 'vs/base/common/iterator';
import { IndexTreeModel, IIndexTreeModelOptions, IList } from 'vs/base/browser/ui/tree/indexTreeModel';
import { IndexTreeModel, IIndexTreeModelOptions, IList, IIndexTreeModelSpliceOptions } from 'vs/base/browser/ui/tree/indexTreeModel';
import { Event } from 'vs/base/common/event';
import { ITreeModel, ITreeNode, ITreeElement, ITreeSorter, ICollapseStateChangeEvent, ITreeModelSpliceEvent, TreeError } from 'vs/base/browser/ui/tree/tree';
import { IIdentityProvider } from 'vs/base/browser/ui/list/list';
@ -13,11 +13,14 @@ import { mergeSort } from 'vs/base/common/arrays';
export type ITreeNodeCallback<T, TFilterData> = (node: ITreeNode<T, TFilterData>) => void;
export interface IObjectTreeModel<T extends NonNullable<any>, TFilterData extends NonNullable<any> = void> extends ITreeModel<T | null, TFilterData, T | null> {
setChildren(element: T | null, children: Iterable<ITreeElement<T>> | undefined): void;
setChildren(element: T | null, children: Iterable<ITreeElement<T>> | undefined, options?: IObjectTreeModelSetChildrenOptions<T, TFilterData>): void;
resort(element?: T | null, recursive?: boolean): void;
updateElementHeight(element: T, height: number): void;
}
export interface IObjectTreeModelSetChildrenOptions<T, TFilterData> extends IIndexTreeModelSpliceOptions<T, TFilterData> {
}
export interface IObjectTreeModelOptions<T, TFilterData> extends IIndexTreeModelOptions<T, TFilterData> {
readonly sorter?: ITreeSorter<T>;
readonly identityProvider?: IIdentityProvider<T>;
@ -63,23 +66,21 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
setChildren(
element: T | null,
children: Iterable<ITreeElement<T>> = Iterable.empty(),
onDidCreateNode?: ITreeNodeCallback<T, TFilterData>,
onDidDeleteNode?: ITreeNodeCallback<T, TFilterData>
options: IObjectTreeModelSetChildrenOptions<T, TFilterData> = {},
): void {
const location = this.getElementLocation(element);
this._setChildren(location, this.preserveCollapseState(children), onDidCreateNode, onDidDeleteNode);
this._setChildren(location, this.preserveCollapseState(children), options);
}
private _setChildren(
location: number[],
children: Iterable<ITreeElement<T>> = Iterable.empty(),
onDidCreateNode?: ITreeNodeCallback<T, TFilterData>,
onDidDeleteNode?: ITreeNodeCallback<T, TFilterData>
options: IObjectTreeModelSetChildrenOptions<T, TFilterData>,
): void {
const insertedElements = new Set<T | null>();
const insertedElementIds = new Set<string>();
const _onDidCreateNode = (node: ITreeNode<T | null, TFilterData>) => {
const onDidCreateNode = (node: ITreeNode<T | null, TFilterData>) => {
if (node.element === null) {
return;
}
@ -95,12 +96,10 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
this.nodesByIdentity.set(id, tnode);
}
if (onDidCreateNode) {
onDidCreateNode(tnode);
}
options.onDidCreateNode?.(tnode);
};
const _onDidDeleteNode = (node: ITreeNode<T | null, TFilterData>) => {
const onDidDeleteNode = (node: ITreeNode<T | null, TFilterData>) => {
if (node.element === null) {
return;
}
@ -118,17 +117,14 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
}
}
if (onDidDeleteNode) {
onDidDeleteNode(tnode);
}
options.onDidDeleteNode?.(tnode);
};
this.model.splice(
[...location, 0],
Number.MAX_VALUE,
children,
_onDidCreateNode,
_onDidDeleteNode
{ ...options, onDidCreateNode, onDidDeleteNode }
);
}
@ -182,7 +178,7 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
const location = this.getElementLocation(element);
const node = this.model.getNode(location);
this._setChildren(location, this.resortChildren(node, recursive));
this._setChildren(location, this.resortChildren(node, recursive), {});
}
private resortChildren(node: ITreeNode<T | null, TFilterData>, recursive: boolean, first = true): Iterable<ITreeElement<T>> {

View File

@ -14,8 +14,8 @@ export interface ITelemetryData {
}
export type WorkbenchActionExecutedClassification = {
id: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
from: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
id: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; };
from: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; };
};
export type WorkbenchActionExecutedEvent = {
@ -63,7 +63,7 @@ export interface IActionChangeEvent {
export class Action extends Disposable implements IAction {
protected _onDidChange = this._register(new Emitter<IActionChangeEvent>());
readonly onDidChange: Event<IActionChangeEvent> = this._onDidChange.event;
readonly onDidChange = this._onDidChange.event;
protected readonly _id: string;
protected _label: string;
@ -179,10 +179,10 @@ export interface IRunEvent {
export class ActionRunner extends Disposable implements IActionRunner {
private _onBeforeRun = this._register(new Emitter<IRunEvent>());
readonly onBeforeRun: Event<IRunEvent> = this._onBeforeRun.event;
readonly onBeforeRun = this._onBeforeRun.event;
private _onDidRun = this._register(new Emitter<IRunEvent>());
readonly onDidRun: Event<IRunEvent> = this._onDidRun.event;
readonly onDidRun = this._onDidRun.event;
async run(action: IAction, context?: any): Promise<any> {
if (!action.enabled) {
@ -246,15 +246,35 @@ export class ActionWithMenuAction extends Action {
}
}
export class SubmenuAction extends Action {
export class SubmenuAction implements IAction {
get actions(): IAction[] {
readonly id: string;
readonly label: string;
readonly class: string | undefined;
readonly tooltip: string = '';
readonly enabled: boolean = true;
readonly checked: boolean = false;
private readonly _actions: readonly IAction[];
constructor(id: string, label: string, actions: readonly IAction[], cssClass?: string) {
this.id = id;
this.label = label;
this.class = cssClass;
this._actions = actions;
}
dispose(): void {
// there is NOTHING to dispose and the SubmenuAction should
// never have anything to dispose as it is a convenience type
// to bridge into the rendering world.
}
get actions(): readonly IAction[] {
return this._actions;
}
constructor(id: string, label: string, private _actions: IAction[], cssClass?: string) {
super(id, label, cssClass, !!_actions?.length);
}
async run(): Promise<any> { }
}
export class EmptySubmenuAction extends Action {
@ -263,3 +283,16 @@ export class EmptySubmenuAction extends Action {
super(EmptySubmenuAction.ID, nls.localize('submenu.empty', '(empty)'), undefined, false);
}
}
export function toAction(props: { id: string, label: string, enabled?: boolean, checked?: boolean, run: Function; }): IAction {
return {
id: props.id,
label: props.label,
class: undefined,
enabled: props.enabled ?? true,
checked: props.checked ?? false,
run: async () => props.run(),
tooltip: props.label,
dispose: () => { }
};
}

View File

@ -152,13 +152,13 @@ export class Throttler {
return result;
};
this.queuedPromise = new Promise(c => {
this.activePromise!.then(onComplete, onComplete).then(c);
this.queuedPromise = new Promise(resolve => {
this.activePromise!.then(onComplete, onComplete).then(resolve);
});
}
return new Promise((c, e) => {
this.queuedPromise!.then(c, e);
return new Promise((resolve, reject) => {
this.queuedPromise!.then(resolve, reject);
});
}
@ -181,7 +181,7 @@ export class Sequencer {
private current: Promise<any> = Promise.resolve(null);
queue<T>(promiseTask: ITask<Promise<T>>): Promise<T> {
return this.current = this.current.then(() => promiseTask());
return this.current = this.current.then(() => promiseTask(), () => promiseTask());
}
}
@ -205,7 +205,7 @@ export class SequencerByKey<TKey> {
}
/**
* A helper to delay execution of a task that is being requested often.
* A helper to delay (debounce) execution of a task that is being requested often.
*
* Following the throttler, now imagine the mail man wants to optimize the number of
* trips proactively. The trip itself can be long, so he decides not to make the trip
@ -248,9 +248,9 @@ export class Delayer<T> implements IDisposable {
this.cancelTimeout();
if (!this.completionPromise) {
this.completionPromise = new Promise((c, e) => {
this.doResolve = c;
this.doReject = e;
this.completionPromise = new Promise((resolve, reject) => {
this.doResolve = resolve;
this.doReject = reject;
}).then(() => {
this.completionPromise = null;
this.doResolve = null;
@ -1013,3 +1013,61 @@ export class IntervalCounter {
}
//#endregion
export type ValueCallback<T = any> = (value: T | Promise<T>) => void;
/**
* Creates a promise whose resolution or rejection can be controlled imperatively.
*/
export class DeferredPromise<T> {
private completeCallback!: ValueCallback<T>;
private errorCallback!: (err: any) => void;
private rejected = false;
private resolved = false;
public get isRejected() {
return this.rejected;
}
public get isResolved() {
return this.resolved;
}
public get isSettled() {
return this.rejected || this.resolved;
}
public p: Promise<T>;
constructor() {
this.p = new Promise<T>((c, e) => {
this.completeCallback = c;
this.errorCallback = e;
});
}
public complete(value: T) {
return new Promise<void>(resolve => {
this.completeCallback(value);
this.resolved = true;
resolve();
});
}
public error(err: any) {
return new Promise<void>(resolve => {
this.errorCallback(err);
this.rejected = true;
resolve();
});
}
public cancel() {
new Promise<void>(resolve => {
this.errorCallback(errors.canceled());
this.rejected = true;
resolve();
});
}
}

View File

@ -34,8 +34,9 @@ export class VSBuffer {
return new VSBuffer(actual);
}
static fromString(source: string): VSBuffer {
if (hasBuffer) {
static fromString(source: string, options?: { dontUseNodeBuffer?: boolean; }): VSBuffer {
const dontUseNodeBuffer = options?.dontUseNodeBuffer || false;
if (!dontUseNodeBuffer && hasBuffer) {
return new VSBuffer(Buffer.from(source));
} else if (hasTextEncoder) {
if (!textEncoder) {

View File

@ -1,135 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { matchesFuzzy, IMatch } from 'vs/base/common/filters';
import { ltrim } from 'vs/base/common/strings';
export const codiconStartMarker = '$(';
export interface IParsedCodicons {
readonly text: string;
readonly codiconOffsets?: readonly number[];
}
export function parseCodicons(text: string): IParsedCodicons {
const firstCodiconIndex = text.indexOf(codiconStartMarker);
if (firstCodiconIndex === -1) {
return { text }; // return early if the word does not include an codicon
}
return doParseCodicons(text, firstCodiconIndex);
}
function doParseCodicons(text: string, firstCodiconIndex: number): IParsedCodicons {
const codiconOffsets: number[] = [];
let textWithoutCodicons: string = '';
function appendChars(chars: string) {
if (chars) {
textWithoutCodicons += chars;
for (const _ of chars) {
codiconOffsets.push(codiconsOffset); // make sure to fill in codicon offsets
}
}
}
let currentCodiconStart = -1;
let currentCodiconValue: string = '';
let codiconsOffset = 0;
let char: string;
let nextChar: string;
let offset = firstCodiconIndex;
const length = text.length;
// Append all characters until the first codicon
appendChars(text.substr(0, firstCodiconIndex));
// example: $(file-symlink-file) my cool $(other-codicon) entry
while (offset < length) {
char = text[offset];
nextChar = text[offset + 1];
// beginning of codicon: some value $( <--
if (char === codiconStartMarker[0] && nextChar === codiconStartMarker[1]) {
currentCodiconStart = offset;
// if we had a previous potential codicon value without
// the closing ')', it was actually not an codicon and
// so we have to add it to the actual value
appendChars(currentCodiconValue);
currentCodiconValue = codiconStartMarker;
offset++; // jump over '('
}
// end of codicon: some value $(some-codicon) <--
else if (char === ')' && currentCodiconStart !== -1) {
const currentCodiconLength = offset - currentCodiconStart + 1; // +1 to include the closing ')'
codiconsOffset += currentCodiconLength;
currentCodiconStart = -1;
currentCodiconValue = '';
}
// within codicon
else if (currentCodiconStart !== -1) {
// Make sure this is a real codicon name
if (/^[a-z0-9\-]$/i.test(char)) {
currentCodiconValue += char;
} else {
// This is not a real codicon, treat it as text
appendChars(currentCodiconValue);
currentCodiconStart = -1;
currentCodiconValue = '';
}
}
// any value outside of codicons
else {
appendChars(char);
}
offset++;
}
// if we had a previous potential codicon value without
// the closing ')', it was actually not an codicon and
// so we have to add it to the actual value
appendChars(currentCodiconValue);
return { text: textWithoutCodicons, codiconOffsets };
}
export function matchesFuzzyCodiconAware(query: string, target: IParsedCodicons, enableSeparateSubstringMatching = false): IMatch[] | null {
const { text, codiconOffsets } = target;
// Return early if there are no codicon markers in the word to match against
if (!codiconOffsets || codiconOffsets.length === 0) {
return matchesFuzzy(query, text, enableSeparateSubstringMatching);
}
// Trim the word to match against because it could have leading
// whitespace now if the word started with an codicon
const wordToMatchAgainstWithoutCodiconsTrimmed = ltrim(text, ' ');
const leadingWhitespaceOffset = text.length - wordToMatchAgainstWithoutCodiconsTrimmed.length;
// match on value without codicons
const matches = matchesFuzzy(query, wordToMatchAgainstWithoutCodiconsTrimmed, enableSeparateSubstringMatching);
// Map matches back to offsets with codicons and trimming
if (matches) {
for (const match of matches) {
const codiconOffset = codiconOffsets[match.start + leadingWhitespaceOffset] /* codicon offsets at index */ + leadingWhitespaceOffset /* overall leading whitespace offset */;
match.start += codiconOffset;
match.end += codiconOffset;
}
}
return matches;
}

View File

@ -3,9 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { codiconStartMarker } from 'vs/base/common/codicon';
import { Emitter, Event } from 'vs/base/common/event';
import { localize } from 'vs/nls';
export interface IIconRegistry {
readonly all: IterableIterator<Codicon>;
@ -47,8 +45,8 @@ const _registry = new Registry();
export const iconRegistry: IIconRegistry = _registry;
export function registerCodicon(id: string, def: Codicon, description?: string): Codicon {
return new Codicon(id, def, description);
export function registerCodicon(id: string, def: Codicon): Codicon {
return new Codicon(id, def);
}
export class Codicon implements CSSIcon {
@ -61,8 +59,44 @@ export class Codicon implements CSSIcon {
public get cssSelector() { return '.codicon.codicon-' + this.id; }
}
export function getClassNamesArray(id: string, modifier?: string) {
const classNames = ['codicon', 'codicon-' + id];
if (modifier) {
classNames.push('codicon-modifier-' + modifier);
}
return classNames;
}
export interface CSSIcon {
readonly classNames: string;
readonly id: string;
}
export namespace CSSIcon {
export const iconIdRegex = /^(codicon\/)?([a-z\-]+)(?:~([a-z\-]+))?$/i;
export function asClassNameArray(icon: CSSIcon): string[] {
if (icon instanceof Codicon) {
return ['codicon', 'codicon-' + icon.id];
}
const match = iconIdRegex.exec(icon.id);
if (!match) {
return asClassNameArray(Codicon.error);
}
let [, , id, modifier] = match;
const classNames = ['codicon', 'codicon-' + id];
if (modifier) {
classNames.push('codicon-modifier-' + modifier);
}
return classNames;
}
export function asClassName(icon: CSSIcon): string {
return asClassNameArray(icon).join(' ');
}
export function asCSSSelector(icon: CSSIcon): string {
return '.' + asClassNameArray(icon).join('.');
}
}
@ -497,36 +531,15 @@ export namespace Codicon {
export const passFilled = new Codicon('pass-filled', { character: '\\ebb3' });
export const circleLargeFilled = new Codicon('circle-large-filled', { character: '\\ebb4' });
export const circleLargeOutline = new Codicon('circle-large-outline', { character: '\\ebb5' });
export const combine = new Codicon('combine', { character: '\\ebb6' });
export const gather = new Codicon('gather', { character: '\\ebb6' });
export const table = new Codicon('table', { character: '\\ebb7' });
export const variableGroup = new Codicon('variable-group', { character: '\\ebb8' });
export const typeHierarchy = new Codicon('type-hierarchy', { character: '\\ebb9' });
export const typeHierarchySub = new Codicon('type-hierarchy-sub', { character: '\\ebba' });
export const typeHierarchySuper = new Codicon('type-hierarchy-super', { character: '\\ebbb' });
export const gitPullRequestCreate = new Codicon('git-pull-request-create', { character: '\\ebbc' });
export const dropDownButton = new Codicon('drop-down-button', Codicon.chevronDown.definition, localize('dropDownButton', 'Icon for drop down buttons.'));
export const dropDownButton = new Codicon('drop-down-button', Codicon.chevronDown.definition);
}
// common icons
const escapeCodiconsRegex = /(\\)?\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)/gi;
export function escapeCodicons(text: string): string {
return text.replace(escapeCodiconsRegex, (match, escaped) => escaped ? match : `\\${match}`);
}
const markdownEscapedCodiconsRegex = /\\\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)/gi;
export function markdownEscapeEscapedCodicons(text: string): string {
// Need to add an extra \ for escaping in markdown
return text.replace(markdownEscapedCodiconsRegex, match => `\\${match}`);
}
const markdownUnescapeCodiconsRegex = /(\\)?\$\\\(([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?)\\\)/gi;
export function markdownUnescapeCodicons(text: string): string {
return text.replace(markdownUnescapeCodiconsRegex, (match, escaped, codicon) => escaped ? match : `$(${codicon})`);
}
const stripCodiconsRegex = /(\s)?(\\)?\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)(\s)?/gi;
export function stripCodicons(text: string): string {
if (text.indexOf(codiconStartMarker) === -1) {
return text;
}
return text.replace(stripCodiconsRegex, (match, preWhitespace, escaped, postWhitespace) => escaped ? match : preWhitespace || postWhitespace || '');
}

View File

@ -142,6 +142,15 @@ export function isPromiseCanceledError(error: any): boolean {
return error instanceof Error && error.name === canceledName && error.message === canceledName;
}
// !!!IMPORTANT!!!
// Do NOT change this class because it is also used as an API-type.
export class CancellationError extends Error {
constructor() {
super(canceledName);
this.name = this.message;
}
}
/**
* Returns an error that signals cancellation.
*/

View File

@ -8,6 +8,7 @@ import { once as onceFn } from 'vs/base/common/functional';
import { Disposable, IDisposable, toDisposable, combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { LinkedList } from 'vs/base/common/linkedList';
import { CancellationToken } from 'vs/base/common/cancellation';
import { StopWatch } from 'vs/base/common/stopwatch';
/**
* To an event a function with one or zero parameters
@ -374,7 +375,7 @@ export namespace Event {
}
export function toPromise<T>(event: Event<T>): Promise<T> {
return new Promise(c => once(event)(c));
return new Promise(resolve => once(event)(resolve));
}
}
@ -386,6 +387,41 @@ export interface EmitterOptions {
onListenerDidAdd?: Function;
onLastListenerRemove?: Function;
leakWarningThreshold?: number;
/** ONLY enable this during development */
_profName?: string
}
class EventProfiling {
private static _idPool = 0;
private _name: string;
private _stopWatch?: StopWatch;
private _listenerCount: number = 0;
private _invocationCount = 0;
private _elapsedOverall = 0;
constructor(name: string) {
this._name = `${name}_${EventProfiling._idPool++}`;
}
start(listenerCount: number): void {
this._stopWatch = new StopWatch(true);
this._listenerCount = listenerCount;
}
stop(): void {
if (this._stopWatch) {
const elapsed = this._stopWatch.elapsed();
this._elapsedOverall += elapsed;
this._invocationCount += 1;
console.info(`did FIRE ${this._name}: elapsed_ms: ${elapsed.toFixed(5)}, listener: ${this._listenerCount} (elapsed_overall: ${this._elapsedOverall.toFixed(2)}, invocations: ${this._invocationCount})`);
this._stopWatch = undefined;
}
}
}
let _globalLeakWarningThreshold = -1;
@ -487,6 +523,7 @@ export class Emitter<T> {
private readonly _options?: EmitterOptions;
private readonly _leakageMon?: LeakageMonitor;
private readonly _perfMon?: EventProfiling;
private _disposed: boolean = false;
private _event?: Event<T>;
private _deliveryQueue?: LinkedList<[Listener<T>, T]>;
@ -494,9 +531,8 @@ export class Emitter<T> {
constructor(options?: EmitterOptions) {
this._options = options;
this._leakageMon = _globalLeakWarningThreshold > 0
? new LeakageMonitor(this._options && this._options.leakWarningThreshold)
: undefined;
this._leakageMon = _globalLeakWarningThreshold > 0 ? new LeakageMonitor(this._options && this._options.leakWarningThreshold) : undefined;
this._perfMon = this._options?._profName ? new EventProfiling(this._options._profName) : undefined;
}
/**
@ -527,10 +563,7 @@ export class Emitter<T> {
}
// check and record this emitter for potential leakage
let removeMonitor: (() => void) | undefined;
if (this._leakageMon) {
removeMonitor = this._leakageMon.check(this._listeners.size);
}
const removeMonitor = this._leakageMon?.check(this._listeners.size);
let result: IDisposable;
result = {
@ -580,6 +613,9 @@ export class Emitter<T> {
this._deliveryQueue.push([listener, event]);
}
// start/stop performance insight collection
this._perfMon?.start(this._deliveryQueue.size);
while (this._deliveryQueue.size > 0) {
const [listener, event] = this._deliveryQueue.shift()!;
try {
@ -592,19 +628,15 @@ export class Emitter<T> {
onUnexpectedError(e);
}
}
this._perfMon?.stop();
}
}
dispose() {
if (this._listeners) {
this._listeners.clear();
}
if (this._deliveryQueue) {
this._deliveryQueue.clear();
}
if (this._leakageMon) {
this._leakageMon.dispose();
}
this._listeners?.clear();
this._deliveryQueue?.clear();
this._leakageMon?.dispose();
this._disposed = true;
}
}
@ -617,7 +649,7 @@ export class PauseableEmitter<T> extends Emitter<T> {
constructor(options?: EmitterOptions & { merge?: (input: T[]) => T }) {
super(options);
this._mergeFn = options && options.merge;
this._mergeFn = options?.merge;
}
pause(): void {

View File

@ -277,14 +277,25 @@ export function isRootOrDriveLetter(path: string): boolean {
return false;
}
return isWindowsDriveLetter(pathNormalized.charCodeAt(0))
&& pathNormalized.charCodeAt(1) === CharCode.Colon
&& (path.length === 2 || pathNormalized.charCodeAt(2) === CharCode.Backslash);
return hasDriveLetter(pathNormalized) &&
(path.length === 2 || pathNormalized.charCodeAt(2) === CharCode.Backslash);
}
return pathNormalized === posix.sep;
}
export function hasDriveLetter(path: string): boolean {
if (isWindows) {
return isWindowsDriveLetter(path.charCodeAt(0)) && path.charCodeAt(1) === CharCode.Colon;
}
return false;
}
export function getDriveLetter(path: string): string | undefined {
return hasDriveLetter(path) ? path[0] : undefined;
}
export function indexOfPath(path: string, candidate: string, ignoreCase?: boolean): number {
if (candidate.length > path.length) {
return -1;

View File

@ -370,24 +370,23 @@ export function anyScore(pattern: string, lowPattern: string, _patternPos: numbe
if (result) {
return result;
}
let matches = 0;
let matches: number[] = [];
let score = 0;
let idx = _wordPos;
for (let patternPos = 0; patternPos < lowPattern.length && patternPos < _maxLen; ++patternPos) {
const wordPos = lowWord.indexOf(lowPattern.charAt(patternPos), idx);
if (wordPos >= 0) {
score += 1;
matches += 2 ** wordPos;
matches.unshift(wordPos);
idx = wordPos + 1;
} else if (matches !== 0) {
} else if (matches.length > 0) {
// once we have started matching things
// we need to match the remaining pattern
// characters
break;
}
}
return [score, matches, _wordPos];
return [score, _wordPos, ...matches];
}
//#region --- fuzzyScore ---
@ -396,19 +395,15 @@ export function createMatches(score: undefined | FuzzyScore): IMatch[] {
if (typeof score === 'undefined') {
return [];
}
const matches = score[1].toString(2);
const wordStart = score[2];
const res: IMatch[] = [];
for (let pos = wordStart; pos < _maxLen; pos++) {
if (matches[matches.length - (pos + 1)] === '1') {
const last = res[res.length - 1];
if (last && last.end === pos) {
last.end = pos + 1;
} else {
res.push({ start: pos, end: pos + 1 });
}
const wordPos = score[1];
for (let i = score.length - 1; i > 1; i--) {
const pos = score[i] + wordPos;
const last = res[res.length - 1];
if (last && last.end === pos) {
last.end = pos + 1;
} else {
res.push({ start: pos, end: pos + 1 });
}
}
return res;
@ -418,20 +413,28 @@ const _maxLen = 128;
function initTable() {
const table: number[][] = [];
const row: number[] = [0];
for (let i = 1; i <= _maxLen; i++) {
row.push(-i);
const row: number[] = [];
for (let i = 0; i <= _maxLen; i++) {
row[i] = 0;
}
for (let i = 0; i <= _maxLen; i++) {
const thisRow = row.slice(0);
thisRow[0] = -i;
table.push(thisRow);
table.push(row.slice(0));
}
return table;
}
function initArr(maxLen: number) {
const row: number[] = [];
for (let i = 0; i <= maxLen; i++) {
row[i] = 0;
}
return row;
}
const _minWordMatchPos = initArr(2 * _maxLen); // min word position for a certain pattern position
const _maxWordMatchPos = initArr(2 * _maxLen); // max word position for a certain pattern position
const _diag = initTable(); // the length of a contiguous diagonal match
const _table = initTable();
const _scores = initTable();
const _arrows = <Arrow[][]>initTable();
const _debug = false;
@ -460,14 +463,14 @@ function printTables(pattern: string, patternStart: number, word: string, wordSt
word = word.substr(wordStart);
console.log(printTable(_table, pattern, pattern.length, word, word.length));
console.log(printTable(_arrows, pattern, pattern.length, word, word.length));
console.log(printTable(_scores, pattern, pattern.length, word, word.length));
console.log(printTable(_diag, pattern, pattern.length, word, word.length));
}
function isSeparatorAtPos(value: string, index: number): boolean {
if (index < 0 || index >= value.length) {
return false;
}
const code = value.charCodeAt(index);
const code = value.codePointAt(index);
switch (code) {
case CharCode.Underline:
case CharCode.Dash:
@ -479,8 +482,16 @@ function isSeparatorAtPos(value: string, index: number): boolean {
case CharCode.DoubleQuote:
case CharCode.Colon:
case CharCode.DollarSign:
case CharCode.LessThan:
case CharCode.OpenParen:
case CharCode.OpenSquareBracket:
return true;
case undefined:
return false;
default:
if (strings.isEmojiImprecise(code)) {
return true;
}
return false;
}
}
@ -503,9 +514,13 @@ function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean {
return word[pos] !== wordLow[pos];
}
export function isPatternInWord(patternLow: string, patternPos: number, patternLen: number, wordLow: string, wordPos: number, wordLen: number): boolean {
export function isPatternInWord(patternLow: string, patternPos: number, patternLen: number, wordLow: string, wordPos: number, wordLen: number, fillMinWordPosArr = false): boolean {
while (patternPos < patternLen && wordPos < wordLen) {
if (patternLow[patternPos] === wordLow[wordPos]) {
if (fillMinWordPosArr) {
// Remember the min word position for each pattern position
_minWordMatchPos[patternPos] = wordPos;
}
patternPos += 1;
}
wordPos += 1;
@ -513,21 +528,24 @@ export function isPatternInWord(patternLow: string, patternPos: number, patternL
return patternPos === patternLen; // pattern must be exhausted
}
const enum Arrow { Top = 0b1, Diag = 0b10, Left = 0b100 }
const enum Arrow { Diag = 1, Left = 2, LeftLeft = 3 }
/**
* A tuple of three values.
* An array representating a fuzzy match.
*
* 0. the score
* 1. the matches encoded as bitmask (2^53)
* 2. the offset at which matching started
* 1. the offset at which matching started
* 2. `<match_pos_N>`
* 3. `<match_pos_1>`
* 4. `<match_pos_0>` etc
*/
export type FuzzyScore = [number, number, number];
export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]];// [number, number, number];
export namespace FuzzyScore {
/**
* No matches and value `-100`
*/
export const Default: [-100, 0, 0] = <[-100, 0, 0]>Object.freeze([-100, 0, 0]);
export const Default: FuzzyScore = ([-100, 0]);
export function isDefault(score?: FuzzyScore): score is [-100, 0, 0] {
return !score || (score[0] === -100 && score[1] === 0 && score[2] === 0);
@ -550,58 +568,71 @@ export function fuzzyScore(pattern: string, patternLow: string, patternStart: nu
// Run a simple check if the characters of pattern occur
// (in order) at all in word. If that isn't the case we
// stop because no match will be possible
if (!isPatternInWord(patternLow, patternStart, patternLen, wordLow, wordStart, wordLen)) {
if (!isPatternInWord(patternLow, patternStart, patternLen, wordLow, wordStart, wordLen, true)) {
return undefined;
}
// Find the max matching word position for each pattern position
// NOTE: the min matching word position was filled in above, in the `isPatternInWord` call
_fillInMaxWordMatchPos(patternLen, wordLen, patternStart, wordStart, patternLow, wordLow);
let row: number = 1;
let column: number = 1;
let patternPos = patternStart;
let wordPos = wordStart;
let hasStrongFirstMatch = false;
const hasStrongFirstMatch = [false];
// There will be a match, fill in tables
for (row = 1, patternPos = patternStart; patternPos < patternLen; row++, patternPos++) {
for (column = 1, wordPos = wordStart; wordPos < wordLen; column++, wordPos++) {
// Reduce search space to possible matching word positions and to possible access from next row
const minWordMatchPos = _minWordMatchPos[patternPos];
const maxWordMatchPos = _maxWordMatchPos[patternPos];
const nextMaxWordMatchPos = (patternPos + 1 < patternLen ? _maxWordMatchPos[patternPos + 1] : wordLen);
const score = _doScore(pattern, patternLow, patternPos, patternStart, word, wordLow, wordPos);
for (column = minWordMatchPos - wordStart + 1, wordPos = minWordMatchPos; wordPos < nextMaxWordMatchPos; column++, wordPos++) {
if (patternPos === patternStart && score > 1) {
hasStrongFirstMatch = true;
let score = Number.MIN_SAFE_INTEGER;
let canComeDiag = false;
if (wordPos <= maxWordMatchPos) {
score = _doScore(
pattern, patternLow, patternPos, patternStart,
word, wordLow, wordPos, wordLen, wordStart,
_diag[row - 1][column - 1] === 0,
hasStrongFirstMatch
);
}
_scores[row][column] = score;
let diagScore = 0;
if (score !== Number.MAX_SAFE_INTEGER) {
canComeDiag = true;
diagScore = score + _table[row - 1][column - 1];
}
const diag = _table[row - 1][column - 1] + (score > 1 ? 1 : score);
const top = _table[row - 1][column] + -1;
const left = _table[row][column - 1] + -1;
const canComeLeft = wordPos > minWordMatchPos;
const leftScore = canComeLeft ? _table[row][column - 1] + (_diag[row][column - 1] > 0 ? -5 : 0) : 0; // penalty for a gap start
if (left >= top) {
// left or diag
if (left > diag) {
_table[row][column] = left;
_arrows[row][column] = Arrow.Left;
} else if (left === diag) {
_table[row][column] = left;
_arrows[row][column] = Arrow.Left | Arrow.Diag;
} else {
_table[row][column] = diag;
_arrows[row][column] = Arrow.Diag;
}
const canComeLeftLeft = wordPos > minWordMatchPos + 1 && _diag[row][column - 1] > 0;
const leftLeftScore = canComeLeftLeft ? _table[row][column - 2] + (_diag[row][column - 2] > 0 ? -5 : 0) : 0; // penalty for a gap start
if (canComeLeftLeft && (!canComeLeft || leftLeftScore >= leftScore) && (!canComeDiag || leftLeftScore >= diagScore)) {
// always prefer choosing left left to jump over a diagonal because that means a match is earlier in the word
_table[row][column] = leftLeftScore;
_arrows[row][column] = Arrow.LeftLeft;
_diag[row][column] = 0;
} else if (canComeLeft && (!canComeDiag || leftScore >= diagScore)) {
// always prefer choosing left since that means a match is earlier in the word
_table[row][column] = leftScore;
_arrows[row][column] = Arrow.Left;
_diag[row][column] = 0;
} else if (canComeDiag) {
_table[row][column] = diagScore;
_arrows[row][column] = Arrow.Diag;
_diag[row][column] = _diag[row - 1][column - 1] + 1;
} else {
// top or diag
if (top > diag) {
_table[row][column] = top;
_arrows[row][column] = Arrow.Top;
} else if (top === diag) {
_table[row][column] = top;
_arrows[row][column] = Arrow.Top | Arrow.Diag;
} else {
_table[row][column] = diag;
_arrows[row][column] = Arrow.Diag;
}
throw new Error(`not possible`);
}
}
}
@ -610,144 +641,152 @@ export function fuzzyScore(pattern: string, patternLow: string, patternStart: nu
printTables(pattern, patternStart, word, wordStart);
}
if (!hasStrongFirstMatch && !firstMatchCanBeWeak) {
if (!hasStrongFirstMatch[0] && !firstMatchCanBeWeak) {
return undefined;
}
_matchesCount = 0;
_topScore = -100;
_wordStart = wordStart;
_firstMatchCanBeWeak = firstMatchCanBeWeak;
row--;
column--;
_findAllMatches2(row - 1, column - 1, patternLen === wordLen ? 1 : 0, 0, false);
if (_matchesCount === 0) {
return undefined;
const result: FuzzyScore = [_table[row][column], wordStart];
let backwardsDiagLength = 0;
let maxMatchColumn = 0;
while (row >= 1) {
// Find the column where we go diagonally up
let diagColumn = column;
do {
const arrow = _arrows[row][diagColumn];
if (arrow === Arrow.LeftLeft) {
diagColumn = diagColumn - 2;
} else if (arrow === Arrow.Left) {
diagColumn = diagColumn - 1;
} else {
// found the diagonal
break;
}
} while (diagColumn >= 1);
// Overturn the "forwards" decision if keeping the "backwards" diagonal would give a better match
if (
backwardsDiagLength > 1 // only if we would have a contiguous match of 3 characters
&& patternLow[patternStart + row - 1] === wordLow[wordStart + column - 1] // only if we can do a contiguous match diagonally
&& !isUpperCaseAtPos(diagColumn + wordStart - 1, word, wordLow) // only if the forwards chose diagonal is not an uppercase
&& backwardsDiagLength + 1 > _diag[row][diagColumn] // only if our contiguous match would be longer than the "forwards" contiguous match
) {
diagColumn = column;
}
if (diagColumn === column) {
// this is a contiguous match
backwardsDiagLength++;
} else {
backwardsDiagLength = 1;
}
if (!maxMatchColumn) {
// remember the last matched column
maxMatchColumn = diagColumn;
}
row--;
column = diagColumn - 1;
result.push(column);
}
return [_topScore, _topMatch2, wordStart];
if (wordLen === patternLen) {
// the word matches the pattern with all characters!
// giving the score a total match boost (to come up ahead other words)
result[0] += 2;
}
// Add 1 penalty for each skipped character in the word
const skippedCharsCount = maxMatchColumn - patternLen;
result[0] -= skippedCharsCount;
return result;
}
function _doScore(pattern: string, patternLow: string, patternPos: number, patternStart: number, word: string, wordLow: string, wordPos: number) {
if (patternLow[patternPos] !== wordLow[wordPos]) {
return -1;
function _fillInMaxWordMatchPos(patternLen: number, wordLen: number, patternStart: number, wordStart: number, patternLow: string, wordLow: string) {
let patternPos = patternLen - 1;
let wordPos = wordLen - 1;
while (patternPos >= patternStart && wordPos >= wordStart) {
if (patternLow[patternPos] === wordLow[wordPos]) {
_maxWordMatchPos[patternPos] = wordPos;
patternPos--;
}
wordPos--;
}
}
function _doScore(
pattern: string, patternLow: string, patternPos: number, patternStart: number,
word: string, wordLow: string, wordPos: number, wordLen: number, wordStart: number,
newMatchStart: boolean,
outFirstMatchStrong: boolean[],
): number {
if (patternLow[patternPos] !== wordLow[wordPos]) {
return Number.MIN_SAFE_INTEGER;
}
let score = 1;
let isGapLocation = false;
if (wordPos === (patternPos - patternStart)) {
// common prefix: `foobar <-> foobaz`
// ^^^^^
if (pattern[patternPos] === word[wordPos]) {
return 7;
} else {
return 5;
}
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
} else if (isUpperCaseAtPos(wordPos, word, wordLow) && (wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow))) {
// hitting upper-case: `foo <-> forOthers`
// ^^ ^
if (pattern[patternPos] === word[wordPos]) {
return 7;
} else {
return 5;
}
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
isGapLocation = true;
} else if (isSeparatorAtPos(wordLow, wordPos) && (wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))) {
// hitting a separator: `. <-> foo.bar`
// ^
return 5;
score = 5;
} else if (isSeparatorAtPos(wordLow, wordPos - 1) || isWhitespaceAtPos(wordLow, wordPos - 1)) {
// post separator: `foo <-> bar_foo`
// ^^^
return 5;
score = 5;
isGapLocation = true;
}
if (score > 1 && patternPos === patternStart) {
outFirstMatchStrong[0] = true;
}
if (!isGapLocation) {
isGapLocation = isUpperCaseAtPos(wordPos, word, wordLow) || isSeparatorAtPos(wordLow, wordPos - 1) || isWhitespaceAtPos(wordLow, wordPos - 1);
}
//
if (patternPos === patternStart) { // first character in pattern
if (wordPos > wordStart) {
// the first pattern character would match a word character that is not at the word start
// so introduce a penalty to account for the gap preceding this match
score -= isGapLocation ? 3 : 5;
}
} else {
return 1;
}
}
let _matchesCount: number = 0;
let _topMatch2: number = 0;
let _topScore: number = 0;
let _wordStart: number = 0;
let _firstMatchCanBeWeak: boolean = false;
function _findAllMatches2(row: number, column: number, total: number, matches: number, lastMatched: boolean): void {
if (_matchesCount >= 10 || total < -25) {
// stop when having already 10 results, or
// when a potential alignment as already 5 gaps
return;
}
let simpleMatchCount = 0;
while (row > 0 && column > 0) {
const score = _scores[row][column];
const arrow = _arrows[row][column];
if (arrow === Arrow.Left) {
// left -> no match, skip a word character
column -= 1;
if (lastMatched) {
total -= 5; // new gap penalty
} else if (matches !== 0) {
total -= 1; // gap penalty after first match
}
lastMatched = false;
simpleMatchCount = 0;
} else if (arrow & Arrow.Diag) {
if (arrow & Arrow.Left) {
// left
_findAllMatches2(
row,
column - 1,
matches !== 0 ? total - 1 : total, // gap penalty after first match
matches,
lastMatched
);
}
// diag
total += score;
row -= 1;
column -= 1;
lastMatched = true;
// match -> set a 1 at the word pos
matches += 2 ** (column + _wordStart);
// count simple matches and boost a row of
// simple matches when they yield in a
// strong match.
if (score === 1) {
simpleMatchCount += 1;
if (row === 0 && !_firstMatchCanBeWeak) {
// when the first match is a weak
// match we discard it
return undefined;
}
} else {
// boost
total += 1 + (simpleMatchCount * (score - 1));
simpleMatchCount = 0;
}
if (newMatchStart) {
// this would be the beginning of a new match (i.e. there would be a gap before this location)
score += isGapLocation ? 2 : 0;
} else {
return undefined;
// this is part of a contiguous match, so give it a slight bonus, but do so only if it would not be a prefered gap location
score += isGapLocation ? 0 : 1;
}
}
total -= column >= 3 ? 9 : column * 3; // late start penalty
// dynamically keep track of the current top score
// and insert the current best score at head, the rest at tail
_matchesCount += 1;
if (total > _topScore) {
_topScore = total;
_topMatch2 = matches;
if (wordPos + 1 === wordLen) {
// we always penalize gaps, but this gives unfair advantages to a match that would match the last character in the word
// so pretend there is a gap after the last character in the word to normalize things
score -= isGapLocation ? 3 : 5;
}
return score;
}
//#endregion

View File

@ -389,7 +389,7 @@ export function scoreItemFuzzy<T>(item: T, query: IPreparedQuery, fuzzy: boolean
// - description (if provided)
// - query (normalized)
// - number of query pieces (i.e. 'hello world' and 'helloworld' are different)
// - wether fuzzy matching is enabled or not
// - whether fuzzy matching is enabled or not
let cacheHash: string;
if (description) {
cacheHash = `${label}${description}${query.normalized}${Array.isArray(query.values) ? query.values.length : ''}${fuzzy}`;

View File

@ -393,15 +393,24 @@ function trivia3(pattern: string, options: IGlobOptions): ParsedStringPattern {
}
// common patterns: **/something/else just need endsWith check, something/else just needs and equals check
function trivia4and5(path: string, pattern: string, matchPathEnds: boolean): ParsedStringPattern {
const nativePath = paths.sep !== paths.posix.sep ? path.replace(ALL_FORWARD_SLASHES, paths.sep) : path;
function trivia4and5(targetPath: string, pattern: string, matchPathEnds: boolean): ParsedStringPattern {
const usingPosixSep = paths.sep === paths.posix.sep;
const nativePath = usingPosixSep ? targetPath : targetPath.replace(ALL_FORWARD_SLASHES, paths.sep);
const nativePathEnd = paths.sep + nativePath;
const parsedPattern: ParsedStringPattern = matchPathEnds ? function (path, basename) {
return typeof path === 'string' && (path === nativePath || path.endsWith(nativePathEnd)) ? pattern : null;
} : function (path, basename) {
return typeof path === 'string' && path === nativePath ? pattern : null;
const targetPathEnd = paths.posix.sep + targetPath;
const parsedPattern: ParsedStringPattern = matchPathEnds ? function (testPath, basename) {
return typeof testPath === 'string' &&
((testPath === nativePath || testPath.endsWith(nativePathEnd))
|| !usingPosixSep && (testPath === targetPath || testPath.endsWith(targetPathEnd)))
? pattern : null;
} : function (testPath, basename) {
return typeof testPath === 'string' &&
(testPath === nativePath
|| (!usingPosixSep && testPath === targetPath))
? pattern : null;
};
parsedPattern.allPaths = [(matchPathEnds ? '*/' : './') + path];
parsedPattern.allPaths = [(matchPathEnds ? '*/' : './') + targetPath];
return parsedPattern;
}

View File

@ -5,7 +5,7 @@
import { equals } from 'vs/base/common/arrays';
import { UriComponents } from 'vs/base/common/uri';
import { escapeCodicons } from 'vs/base/common/codicons';
import { escapeIcons } from 'vs/base/common/iconLabels';
import { illegalArgument } from 'vs/base/common/errors';
export interface IMarkdownString {
@ -46,9 +46,7 @@ export class MarkdownString implements IMarkdownString {
}
appendText(value: string, newlineStyle: MarkdownStringTextNewlineStyle = MarkdownStringTextNewlineStyle.Paragraph): MarkdownString {
// escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash
this.value += (this.supportThemeIcons ? escapeCodicons(value) : value)
.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&')
this.value += escapeMarkdownSyntaxTokens(this.supportThemeIcons ? escapeIcons(value) : value)
.replace(/([ \t]+)/g, (_match, g1) => '&nbsp;'.repeat(g1.length))
.replace(/^>/gm, '\\>')
.replace(/\n/g, newlineStyle === MarkdownStringTextNewlineStyle.Break ? '\\\n' : '\n\n');
@ -116,6 +114,11 @@ function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean {
}
}
export function escapeMarkdownSyntaxTokens(text: string): string {
// escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash
return text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&');
}
export function removeMarkdownEscapes(text: string): string {
if (!text) {
return text;

View File

@ -0,0 +1,161 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { matchesFuzzy, IMatch } from 'vs/base/common/filters';
import { ltrim } from 'vs/base/common/strings';
export const iconStartMarker = '$(';
const escapeIconsRegex = /(\\)?\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)/gi;
export function escapeIcons(text: string): string {
return text.replace(escapeIconsRegex, (match, escaped) => escaped ? match : `\\${match}`);
}
const markdownEscapedIconsRegex = /\\\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)/gi;
export function markdownEscapeEscapedIcons(text: string): string {
// Need to add an extra \ for escaping in markdown
return text.replace(markdownEscapedIconsRegex, match => `\\${match}`);
}
const markdownUnescapeIconsRegex = /(\\)?\$\\\(([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?)\\\)/gi;
export function markdownUnescapeIcons(text: string): string {
return text.replace(markdownUnescapeIconsRegex, (match, escaped, iconId) => escaped ? match : `$(${iconId})`);
}
const stripIconsRegex = /(\s)?(\\)?\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)(\s)?/gi;
export function stripIcons(text: string): string {
if (text.indexOf(iconStartMarker) === -1) {
return text;
}
return text.replace(stripIconsRegex, (match, preWhitespace, escaped, postWhitespace) => escaped ? match : preWhitespace || postWhitespace || '');
}
export interface IParsedLabelWithIcons {
readonly text: string;
readonly iconOffsets?: readonly number[];
}
export function parseLabelWithIcons(text: string): IParsedLabelWithIcons {
const firstIconIndex = text.indexOf(iconStartMarker);
if (firstIconIndex === -1) {
return { text }; // return early if the word does not include an icon
}
return doParseLabelWithIcons(text, firstIconIndex);
}
function doParseLabelWithIcons(text: string, firstIconIndex: number): IParsedLabelWithIcons {
const iconOffsets: number[] = [];
let textWithoutIcons: string = '';
function appendChars(chars: string) {
if (chars) {
textWithoutIcons += chars;
for (const _ of chars) {
iconOffsets.push(iconsOffset); // make sure to fill in icon offsets
}
}
}
let currentIconStart = -1;
let currentIconValue: string = '';
let iconsOffset = 0;
let char: string;
let nextChar: string;
let offset = firstIconIndex;
const length = text.length;
// Append all characters until the first icon
appendChars(text.substr(0, firstIconIndex));
// example: $(file-symlink-file) my cool $(other-icon) entry
while (offset < length) {
char = text[offset];
nextChar = text[offset + 1];
// beginning of icon: some value $( <--
if (char === iconStartMarker[0] && nextChar === iconStartMarker[1]) {
currentIconStart = offset;
// if we had a previous potential icon value without
// the closing ')', it was actually not an icon and
// so we have to add it to the actual value
appendChars(currentIconValue);
currentIconValue = iconStartMarker;
offset++; // jump over '('
}
// end of icon: some value $(some-icon) <--
else if (char === ')' && currentIconStart !== -1) {
const currentIconLength = offset - currentIconStart + 1; // +1 to include the closing ')'
iconsOffset += currentIconLength;
currentIconStart = -1;
currentIconValue = '';
}
// within icon
else if (currentIconStart !== -1) {
// Make sure this is a real icon name
if (/^[a-z0-9\-]$/i.test(char)) {
currentIconValue += char;
} else {
// This is not a real icon, treat it as text
appendChars(currentIconValue);
currentIconStart = -1;
currentIconValue = '';
}
}
// any value outside of icon
else {
appendChars(char);
}
offset++;
}
// if we had a previous potential icon value without
// the closing ')', it was actually not an icon and
// so we have to add it to the actual value
appendChars(currentIconValue);
return { text: textWithoutIcons, iconOffsets };
}
export function matchesFuzzyIconAware(query: string, target: IParsedLabelWithIcons, enableSeparateSubstringMatching = false): IMatch[] | null {
const { text, iconOffsets } = target;
// Return early if there are no icon markers in the word to match against
if (!iconOffsets || iconOffsets.length === 0) {
return matchesFuzzy(query, text, enableSeparateSubstringMatching);
}
// Trim the word to match against because it could have leading
// whitespace now if the word started with an icon
const wordToMatchAgainstWithoutIconsTrimmed = ltrim(text, ' ');
const leadingWhitespaceOffset = text.length - wordToMatchAgainstWithoutIconsTrimmed.length;
// match on value without icon
const matches = matchesFuzzy(query, wordToMatchAgainstWithoutIconsTrimmed, enableSeparateSubstringMatching);
// Map matches back to offsets with icon and trimming
if (matches) {
for (const match of matches) {
const iconOffset = iconOffsets[match.start + leadingWhitespaceOffset] /* icon offsets at index */ + leadingWhitespaceOffset /* overall leading whitespace offset */;
match.start += iconOffset;
match.end += iconOffset;
}
}
return matches;
}

View File

@ -22,6 +22,10 @@ export namespace Iterable {
return iterable || _empty;
}
export function isEmpty<T>(iterable: Iterable<T> | undefined | null): boolean {
return !iterable || iterable[Symbol.iterator]().next().done === true;
}
export function first<T>(iterable: Iterable<T>): T | undefined {
return iterable[Symbol.iterator]().next().value;
}
@ -35,6 +39,8 @@ export namespace Iterable {
return false;
}
export function filter<T, R extends T>(iterable: Iterable<T>, predicate: (t: T) => t is R): Iterable<R>;
export function filter<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): Iterable<T>;
export function* filter<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): Iterable<T> {
for (const element of iterable) {
if (predicate(element)) {
@ -57,6 +63,33 @@ export namespace Iterable {
}
}
export function* concatNested<T>(iterables: Iterable<Iterable<T>>): Iterable<T> {
for (const iterable of iterables) {
for (const element of iterable) {
yield element;
}
}
}
/**
* Returns an iterable slice of the array, with the same semantics as `array.slice()`.
*/
export function* slice<T>(iterable: ReadonlyArray<T>, from: number, to = iterable.length): Iterable<T> {
if (from < 0) {
from += iterable.length;
}
if (to < 0) {
to += iterable.length;
} else if (to > iterable.length) {
to = iterable.length;
}
for (; from < to; from++) {
yield iterable[from];
}
}
/**
* Consumes `atMost` elements from iterable and returns the consumed elements,
* and an iterable for the rest of the elements.

View File

@ -9,11 +9,12 @@ import { startsWithIgnoreCase, rtrim } from 'vs/base/common/strings';
import { Schemas } from 'vs/base/common/network';
import { isLinux, isWindows, isMacintosh } from 'vs/base/common/platform';
import { isEqual, basename, relativePath } from 'vs/base/common/resources';
import { hasDriveLetter, isRootOrDriveLetter } from 'vs/base/common/extpath';
export interface IWorkspaceFolderProvider {
getWorkspaceFolder(resource: URI): { uri: URI, name?: string } | null;
getWorkspaceFolder(resource: URI): { uri: URI, name?: string; } | null;
getWorkspace(): {
folders: { uri: URI, name?: string }[];
folders: { uri: URI, name?: string; }[];
};
}
@ -84,21 +85,13 @@ export function getBaseLabel(resource: URI | string | undefined): string | undef
const base = basename(resource) || (resource.scheme === Schemas.file ? resource.fsPath : resource.path) /* can be empty string if '/' is passed in */;
// convert c: => C:
if (hasDriveLetter(base)) {
if (isWindows && isRootOrDriveLetter(base)) {
return normalizeDriveLetter(base);
}
return base;
}
function hasDriveLetter(path: string): boolean {
return !!(isWindows && path && path[1] === ':');
}
export function extractDriveLetter(path: string): string | undefined {
return hasDriveLetter(path) ? path[0] : undefined;
}
export function normalizeDriveLetter(path: string): string {
if (hasDriveLetter(path)) {
return path.charAt(0).toUpperCase() + path.slice(1);
@ -107,7 +100,7 @@ export function normalizeDriveLetter(path: string): string {
return path;
}
let normalizedUserHomeCached: { original: string; normalized: string } = Object.create(null);
let normalizedUserHomeCached: { original: string; normalized: string; } = Object.create(null);
export function tildify(path: string, userHome: string): string {
if (isWindows || !path || !userHome) {
return path; // unsupported
@ -286,7 +279,7 @@ interface ISegment {
* @param value string to which templating is applied
* @param values the values of the templates to use
*/
export function template(template: string, values: { [key: string]: string | ISeparator | undefined | null } = Object.create(null)): string {
export function template(template: string, values: { [key: string]: string | ISeparator | undefined | null; } = Object.create(null)): string {
const segments: ISegment[] = [];
let inVariable = false;
@ -390,7 +383,7 @@ export function unmnemonicLabel(label: string): string {
/**
* Splits a path in name and parent path, supporting both '/' and '\'
*/
export function splitName(fullPath: string): { name: string, parentPath: string } {
export function splitName(fullPath: string): { name: string, parentPath: string; } {
const p = fullPath.indexOf('/') !== -1 ? posix : win32;
const name = p.basename(fullPath);
const parentPath = p.dirname(fullPath);

View File

@ -14,34 +14,53 @@ import { Iterable } from 'vs/base/common/iterator';
* extend Disposable or use a DisposableStore. This means there are a lot of false positives.
*/
const TRACK_DISPOSABLES = false;
let disposableTracker: IDisposableTracker | null = null;
const __is_disposable_tracked__ = '__is_disposable_tracked__';
function markTracked<T extends IDisposable>(x: T): void {
if (!TRACK_DISPOSABLES) {
return;
}
if (x && x !== Disposable.None) {
try {
(x as any)[__is_disposable_tracked__] = true;
} catch {
// noop
}
}
export interface IDisposableTracker {
trackDisposable(x: IDisposable): void;
markTracked(x: IDisposable): void;
}
function trackDisposable<T extends IDisposable>(x: T): T {
if (!TRACK_DISPOSABLES) {
export function setDisposableTracker(tracker: IDisposableTracker | null): void {
disposableTracker = tracker;
}
if (TRACK_DISPOSABLES) {
const __is_disposable_tracked__ = '__is_disposable_tracked__';
disposableTracker = new class implements IDisposableTracker {
trackDisposable(x: IDisposable): void {
const stack = new Error('Potentially leaked disposable').stack!;
setTimeout(() => {
if (!(x as any)[__is_disposable_tracked__]) {
console.log(stack);
}
}, 3000);
}
markTracked(x: IDisposable): void {
if (x && x !== Disposable.None) {
try {
(x as any)[__is_disposable_tracked__] = true;
} catch {
// noop
}
}
}
};
}
function markTracked<T extends IDisposable>(x: T): void {
if (!disposableTracker) {
return;
}
disposableTracker.markTracked(x);
}
export function trackDisposable<T extends IDisposable>(x: T): T {
if (!disposableTracker) {
return x;
}
const stack = new Error('Potentially leaked disposable').stack!;
setTimeout(() => {
if (!(x as any)[__is_disposable_tracked__]) {
console.log(stack);
}
}, 3000);
disposableTracker.trackDisposable(x);
return x;
}
@ -98,7 +117,7 @@ export function dispose<T extends IDisposable>(arg: T | IterableIterator<T> | un
export function combinedDisposable(...disposables: IDisposable[]): IDisposable {
disposables.forEach(markTracked);
return trackDisposable({ dispose: () => dispose(disposables) });
return toDisposable(() => dispose(disposables));
}
export function toDisposable(fn: () => void): IDisposable {

View File

@ -60,6 +60,8 @@ export namespace Schemas {
export const vscodeNotebookCell = 'vscode-notebook-cell';
export const vscodeNotebookCellMetadata = 'vscode-notebook-cell-metadata';
export const vscodeSettings = 'vscode-settings';
export const webviewPanel = 'webview-panel';
@ -148,8 +150,8 @@ class FileAccessImpl {
* **Note:** use `dom.ts#asCSSUrl` whenever the URL is to be used in CSS context.
*/
asBrowserUri(uri: URI): URI;
asBrowserUri(moduleId: string, moduleIdToUrl: { toUrl(moduleId: string): string }): URI;
asBrowserUri(uriOrModule: URI | string, moduleIdToUrl?: { toUrl(moduleId: string): string }): URI {
asBrowserUri(moduleId: string, moduleIdToUrl: { toUrl(moduleId: string): string }, __forceCodeFileUri?: boolean): URI;
asBrowserUri(uriOrModule: URI | string, moduleIdToUrl?: { toUrl(moduleId: string): string }, __forceCodeFileUri?: boolean): URI {
const uri = this.toUri(uriOrModule, moduleIdToUrl);
// Handle remote URIs via `RemoteAuthorities`
@ -158,37 +160,23 @@ class FileAccessImpl {
}
// Only convert the URI if we are in a native context and it has `file:` scheme
if (platform.isElectronSandboxed && platform.isNative && uri.scheme === Schemas.file) {
return this.toCodeFileUri(uri);
// and we have explicitly enabled the conversion (sandbox, or ENABLE_VSCODE_BROWSER_CODE_LOADING)
if (platform.isNative && (__forceCodeFileUri || platform.isPreferringBrowserCodeLoad) && uri.scheme === Schemas.file) {
return uri.with({
scheme: Schemas.vscodeFileResource,
// We need to provide an authority here so that it can serve
// as origin for network and loading matters in chromium.
// If the URI is not coming with an authority already, we
// add our own
authority: uri.authority || this.FALLBACK_AUTHORITY,
query: null,
fragment: null
});
}
return uri;
}
/**
* TODO@bpasero remove me eventually when vscode-file is adopted everywhere
*/
_asCodeFileUri(uri: URI): URI;
_asCodeFileUri(moduleId: string, moduleIdToUrl: { toUrl(moduleId: string): string }): URI;
_asCodeFileUri(uriOrModule: URI | string, moduleIdToUrl?: { toUrl(moduleId: string): string }): URI {
const uri = this.toUri(uriOrModule, moduleIdToUrl);
return this.toCodeFileUri(uri);
}
private toCodeFileUri(uri: URI): URI {
return uri.with({
scheme: Schemas.vscodeFileResource,
// We need to provide an authority here so that it can serve
// as origin for network and loading matters in chromium.
// If the URI is not coming with an authority already, we
// add our own
authority: uri.authority || this.FALLBACK_AUTHORITY,
query: null,
fragment: null
});
}
/**
* Returns the `file` URI to use in contexts where node.js
* is responsible for loading.

View File

@ -43,7 +43,7 @@ const CHAR_QUESTION_MARK = 63; /* ? */
class ErrorInvalidArgType extends Error {
code: 'ERR_INVALID_ARG_TYPE';
constructor(name: string, expected: string, actual: any) {
constructor(name: string, expected: string, actual: unknown) {
// determiner: 'must be' or 'must not be'
let determiner;
if (typeof expected === 'string' && expected.indexOf('not ') === 0) {
@ -215,7 +215,7 @@ export const win32: IPath = {
// absolute path, get cwd for that drive, or the process cwd if
// the drive cwd is not available. We're sure the device is not
// a UNC path at this points, because UNC paths are always absolute.
path = (process.env as any)[`=${resolvedDevice}`] || process.cwd();
path = process.env[`=${resolvedDevice}`] || process.cwd();
// Verify that a cwd was found and that it actually points
// to our drive. If not, default to the drive's root.

View File

@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface PerformanceEntry {
export interface PerformanceMark {
readonly name: string;
readonly startTime: number;
}
@ -11,12 +11,6 @@ export interface PerformanceEntry {
export function mark(name: string): void;
/**
* All entries filtered by type and sorted by `startTime`.
* Returns all marks, sorted by `startTime`.
*/
export function getEntries(): PerformanceEntry[];
export function getDuration(from: string, to: string): number;
type ExportData = any[];
export function importEntries(data: ExportData): void;
export function exportEntries(): ExportData;
export function getMarks(): PerformanceMark[];

View File

@ -7,66 +7,88 @@
//@ts-check
function _factory(sharedObj) {
/**
* @returns {{mark(name:string):void, getMarks():{name:string, startTime:number}[]}}
*/
function _definePolyfillMarks(timeOrigin) {
sharedObj.MonacoPerformanceMarks = sharedObj.MonacoPerformanceMarks || [];
const _dataLen = 2;
const _nativeMark = typeof performance === 'object' && typeof performance.mark === 'function' ? performance.mark.bind(performance) : () => { };
function importEntries(entries) {
sharedObj.MonacoPerformanceMarks.splice(0, 0, ...entries);
const _data = [];
if (typeof timeOrigin === 'number') {
_data.push('code/timeOrigin', timeOrigin);
}
function exportEntries() {
return sharedObj.MonacoPerformanceMarks.slice(0);
function mark(name) {
_data.push(name, Date.now());
}
function getEntries() {
function getMarks() {
const result = [];
const entries = sharedObj.MonacoPerformanceMarks;
for (let i = 0; i < entries.length; i += _dataLen) {
for (let i = 0; i < _data.length; i += 2) {
result.push({
name: entries[i],
startTime: entries[i + 1],
name: _data[i],
startTime: _data[i + 1],
});
}
return result;
}
return { mark, getMarks };
}
function getDuration(from, to) {
const entries = sharedObj.MonacoPerformanceMarks;
let target = to;
let endIndex = 0;
for (let i = entries.length - _dataLen; i >= 0; i -= _dataLen) {
if (entries[i] === target) {
if (target === to) {
// found `to` (end of interval)
endIndex = i;
target = from;
} else {
// found `from` (start of interval)
return entries[endIndex + 1] - entries[i + 1];
/**
* @returns {{mark(name:string):void, getMarks():{name:string, startTime:number}[]}}
*/
function _define() {
if (typeof performance === 'object' && typeof performance.mark === 'function') {
// in a browser context, reuse performance-util
if (typeof performance.timeOrigin !== 'number' && !performance.timing) {
// safari & webworker: because there is no timeOrigin and no workaround
// we use the `Date.now`-based polyfill.
return _definePolyfillMarks();
} else {
// use "native" performance for mark and getMarks
return {
mark(name) {
performance.mark(name);
},
getMarks() {
let timeOrigin = performance.timeOrigin;
if (typeof timeOrigin !== 'number') {
// safari: there is no timerOrigin but in renderers there is the timing-property
// see https://bugs.webkit.org/show_bug.cgi?id=174862
timeOrigin = performance.timing.navigationStart || performance.timing.redirectStart || performance.timing.fetchStart;
}
const result = [{ name: 'code/timeOrigin', startTime: Math.round(timeOrigin) }];
for (const entry of performance.getEntriesByType('mark')) {
result.push({
name: entry.name,
startTime: Math.round(timeOrigin + entry.startTime)
});
}
return result;
}
}
};
}
return 0;
} else if (typeof process === 'object') {
// node.js: use the normal polyfill but add the timeOrigin
// from the node perf_hooks API as very first mark
const timeOrigin = Math.round((require.nodeRequire || require)('perf_hooks').performance.timeOrigin);
return _definePolyfillMarks(timeOrigin);
} else {
// unknown environment
console.trace('perf-util loaded in UNKNOWN environment');
return _definePolyfillMarks();
}
}
function mark(name) {
sharedObj.MonacoPerformanceMarks.push(name, Date.now());
_nativeMark(name);
function _factory(sharedObj) {
if (!sharedObj.MonacoPerformanceMarks) {
sharedObj.MonacoPerformanceMarks = _define();
}
const exports = {
mark: mark,
getEntries: getEntries,
getDuration: getDuration,
importEntries: importEntries,
exportEntries: exportEntries
};
return exports;
return sharedObj.MonacoPerformanceMarks;
}
// This module can be loaded in an amd and commonjs-context.
@ -92,5 +114,6 @@ if (typeof define === 'function') {
// commonjs
module.exports = _factory(sharedObj);
} else {
console.trace('perf-util defined in UNKNOWN context (neither requirejs or commonjs)');
sharedObj.perf = _factory(sharedObj);
}

View File

@ -8,6 +8,7 @@ const LANGUAGE_DEFAULT = 'en';
let _isWindows = false;
let _isMacintosh = false;
let _isLinux = false;
let _isLinuxSnap = false;
let _isNative = false;
let _isWeb = false;
let _isIOS = false;
@ -61,6 +62,26 @@ if (typeof process !== 'undefined') {
const isElectronRenderer = typeof nodeProcess?.versions?.electron === 'string' && nodeProcess.type === 'renderer';
export const isElectronSandboxed = isElectronRenderer && nodeProcess?.sandboxed;
export const browserCodeLoadingCacheStrategy: 'none' | 'code' | 'bypassHeatCheck' | 'bypassHeatCheckAndEagerCompile' | undefined = (() => {
// Always enabled when sandbox is enabled
if (isElectronSandboxed) {
return 'bypassHeatCheck';
}
// Otherwise, only enabled conditionally
const env = nodeProcess?.env['ENABLE_VSCODE_BROWSER_CODE_LOADING'];
if (typeof env === 'string') {
if (env === 'none' || env === 'code' || env === 'bypassHeatCheck' || env === 'bypassHeatCheckAndEagerCompile') {
return env;
}
return 'bypassHeatCheck';
}
return undefined;
})();
export const isPreferringBrowserCodeLoad = typeof browserCodeLoadingCacheStrategy === 'string';
// Web environment
if (typeof navigator === 'object' && !isElectronRenderer) {
@ -91,6 +112,7 @@ else if (typeof nodeProcess === 'object') {
_isWindows = (nodeProcess.platform === 'win32');
_isMacintosh = (nodeProcess.platform === 'darwin');
_isLinux = (nodeProcess.platform === 'linux');
_isLinuxSnap = _isLinux && !!nodeProcess.env['SNAP'] && !!nodeProcess.env['SNAP_REVISION'];
_locale = LANGUAGE_DEFAULT;
_language = LANGUAGE_DEFAULT;
const rawNlsConfig = nodeProcess.env['VSCODE_NLS_CONFIG'];
@ -140,6 +162,7 @@ if (_isMacintosh) {
export const isWindows = _isWindows;
export const isMacintosh = _isMacintosh;
export const isLinux = _isLinux;
export const isLinuxSnap = _isLinuxSnap;
export const isNative = _isNative;
export const isWeb = _isWeb;
export const isIOS = _isIOS;

View File

@ -108,7 +108,6 @@ export function sanitizeProcessEnvironment(env: IProcessEnvironment, ...preserve
}, {} as Record<string, boolean>);
const keysToRemove = [
/^ELECTRON_.+$/,
/^GOOGLE_API_KEY$/,
/^VSCODE_.+$/,
/^SNAP(|_.*)$/,
/^GDK_PIXBUF_.+$/,

View File

@ -13,6 +13,8 @@ export const enum ScrollbarVisibility {
}
export interface ScrollEvent {
inSmoothScrolling: boolean;
oldWidth: number;
oldScrollWidth: number;
oldScrollLeft: number;
@ -132,7 +134,7 @@ export class ScrollState implements IScrollDimensions, IScrollPosition {
);
}
public createScrollEvent(previous: ScrollState): ScrollEvent {
public createScrollEvent(previous: ScrollState, inSmoothScrolling: boolean): ScrollEvent {
const widthChanged = (this.width !== previous.width);
const scrollWidthChanged = (this.scrollWidth !== previous.scrollWidth);
const scrollLeftChanged = (this.scrollLeft !== previous.scrollLeft);
@ -142,6 +144,7 @@ export class ScrollState implements IScrollDimensions, IScrollPosition {
const scrollTopChanged = (this.scrollTop !== previous.scrollTop);
return {
inSmoothScrolling: inSmoothScrolling,
oldWidth: previous.width,
oldScrollWidth: previous.scrollWidth,
oldScrollLeft: previous.scrollLeft,
@ -242,7 +245,7 @@ export class Scrollable extends Disposable {
public setScrollDimensions(dimensions: INewScrollDimensions, useRawScrollPositions: boolean): void {
const newState = this._state.withScrollDimensions(dimensions, useRawScrollPositions);
this._setState(newState);
this._setState(newState, Boolean(this._smoothScrolling));
// Validate outstanding animated scroll position target
if (this._smoothScrolling) {
@ -279,10 +282,10 @@ export class Scrollable extends Disposable {
this._smoothScrolling = null;
}
this._setState(newState);
this._setState(newState, false);
}
public setScrollPositionSmooth(update: INewScrollPosition): void {
public setScrollPositionSmooth(update: INewScrollPosition, reuseAnimation?: boolean): void {
if (this._smoothScrollDuration === 0) {
// Smooth scrolling not supported.
return this.setScrollPositionNow(update);
@ -302,8 +305,12 @@ export class Scrollable extends Disposable {
// No need to interrupt or extend the current animation since we're going to the same place
return;
}
const newSmoothScrolling = this._smoothScrolling.combine(this._state, validTarget, this._smoothScrollDuration);
let newSmoothScrolling: SmoothScrollingOperation;
if (reuseAnimation) {
newSmoothScrolling = new SmoothScrollingOperation(this._smoothScrolling.from, validTarget, this._smoothScrolling.startTime, this._smoothScrolling.duration);
} else {
newSmoothScrolling = this._smoothScrolling.combine(this._state, validTarget, this._smoothScrollDuration);
}
this._smoothScrolling.dispose();
this._smoothScrolling = newSmoothScrolling;
} else {
@ -330,7 +337,7 @@ export class Scrollable extends Disposable {
const update = this._smoothScrolling.tick();
const newState = this._state.withScrollPosition(update);
this._setState(newState);
this._setState(newState, true);
if (!this._smoothScrolling) {
// Looks like someone canceled the smooth scrolling
@ -354,14 +361,14 @@ export class Scrollable extends Disposable {
});
}
private _setState(newState: ScrollState): void {
private _setState(newState: ScrollState, inSmoothScrolling: boolean): void {
const oldState = this._state;
if (oldState.equals(newState)) {
// no change
return;
}
this._state = newState;
this._onScroll.fire(this._state.createScrollEvent(oldState));
this._onScroll.fire(this._state.createScrollEvent(oldState, inSmoothScrolling));
}
}
@ -404,17 +411,17 @@ export class SmoothScrollingOperation {
public readonly from: ISmoothScrollPosition;
public to: ISmoothScrollPosition;
public readonly duration: number;
private readonly _startTime: number;
public readonly startTime: number;
public animationFrameDisposable: IDisposable | null;
private scrollLeft!: IAnimation;
private scrollTop!: IAnimation;
protected constructor(from: ISmoothScrollPosition, to: ISmoothScrollPosition, startTime: number, duration: number) {
constructor(from: ISmoothScrollPosition, to: ISmoothScrollPosition, startTime: number, duration: number) {
this.from = from;
this.to = to;
this.duration = duration;
this._startTime = startTime;
this.startTime = startTime;
this.animationFrameDisposable = null;
@ -460,7 +467,7 @@ export class SmoothScrollingOperation {
}
protected _tick(now: number): SmoothScrollingUpdate {
const completion = (now - this._startTime) / this.duration;
const completion = (now - this.startTime) / this.duration;
if (completion < 1) {
const newScrollLeft = this.scrollLeft(completion);

View File

@ -35,6 +35,6 @@ export class StopWatch {
}
private _now(): number {
return this._highResolution ? globals.performance.now() : new Date().getTime();
return this._highResolution ? globals.performance.now() : Date.now();
}
}

View File

@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { onUnexpectedError } from 'vs/base/common/errors';
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
/**
@ -229,7 +230,7 @@ class WriteableStreamImpl<T> implements WriteableStream<T> {
// flowing: directly send the data to listeners
if (this.state.flowing) {
this.listeners.data.forEach(listener => listener(data));
this.emitData(data);
}
// not yet flowing: buffer data until flowing
@ -250,7 +251,7 @@ class WriteableStreamImpl<T> implements WriteableStream<T> {
// flowing: directly send the error to listeners
if (this.state.flowing) {
this.listeners.error.forEach(listener => listener(error));
this.emitError(error);
}
// not yet flowing: buffer errors until flowing
@ -273,7 +274,7 @@ class WriteableStreamImpl<T> implements WriteableStream<T> {
// flowing: send end event to listeners
if (this.state.flowing) {
this.listeners.end.forEach(listener => listener());
this.emitEnd();
this.destroy();
}
@ -284,6 +285,22 @@ class WriteableStreamImpl<T> implements WriteableStream<T> {
}
}
private emitData(data: T): void {
this.listeners.data.slice(0).forEach(listener => listener(data)); // slice to avoid listener mutation from delivering event
}
private emitError(error: Error): void {
if (this.listeners.error.length === 0) {
onUnexpectedError(error); // nobody listened to this error so we log it as unexpected
} else {
this.listeners.error.slice(0).forEach(listener => listener(error)); // slice to avoid listener mutation from delivering event
}
}
private emitEnd(): void {
this.listeners.end.slice(0).forEach(listener => listener()); // slice to avoid listener mutation from delivering event
}
on(event: 'data', callback: (data: T) => void): void;
on(event: 'error', callback: (err: Error) => void): void;
on(event: 'end', callback: () => void): void;
@ -361,7 +378,7 @@ class WriteableStreamImpl<T> implements WriteableStream<T> {
if (this.buffer.data.length > 0) {
const fullDataBuffer = this.reducer(this.buffer.data);
this.listeners.data.forEach(listener => listener(fullDataBuffer));
this.emitData(fullDataBuffer);
this.buffer.data.length = 0;
@ -375,7 +392,7 @@ class WriteableStreamImpl<T> implements WriteableStream<T> {
private flowErrors(): void {
if (this.listeners.error.length > 0) {
for (const error of this.buffer.error) {
this.listeners.error.forEach(listener => listener(error));
this.emitError(error);
}
this.buffer.error.length = 0;
@ -384,7 +401,7 @@ class WriteableStreamImpl<T> implements WriteableStream<T> {
private flowEnd(): boolean {
if (this.state.ended) {
this.listeners.end.forEach(listener => listener());
this.emitEnd();
return this.listeners.end.length > 0;
}
@ -478,9 +495,13 @@ export function consumeStream<T>(stream: ReadableStreamEvents<T>, reducer: IRedu
return new Promise((resolve, reject) => {
const chunks: T[] = [];
stream.on('data', data => chunks.push(data));
stream.on('error', error => reject(error));
stream.on('end', () => resolve(reducer(chunks)));
// Adding the `data` listener will turn the stream
// into flowing mode. As such it is important to
// add this listener last (DO NOT CHANGE!)
stream.on('data', data => chunks.push(data));
});
}
@ -574,3 +595,40 @@ export function transform<Original, Transformed>(stream: ReadableStreamEvents<Or
return target;
}
export interface IReadableStreamObservable {
/**
* A promise to await the `end` or `error` event
* of a stream.
*/
errorOrEnd: () => Promise<void>;
}
/**
* Helper to observe a stream for certain events through
* a promise based API.
*/
export function observe(stream: ReadableStream<unknown>): IReadableStreamObservable {
// A stream is closed when it ended or errord
// We install this listener right from the
// beginning to catch the events early.
const errorOrEnd = Promise.race([
new Promise<void>(resolve => stream.on('end', () => resolve())),
new Promise<void>(resolve => stream.on('error', () => resolve()))
]);
return {
errorOrEnd(): Promise<void> {
// We need to ensure the stream is flowing so that our
// listeners are getting triggered. It is possible that
// the stream is not flowing because no `data` listener
// was attached yet.
stream.resume();
return errorOrEnd;
}
};
}

View File

@ -108,7 +108,7 @@ export class URI implements UriComponents {
&& typeof (<URI>thing).path === 'string'
&& typeof (<URI>thing).query === 'string'
&& typeof (<URI>thing).scheme === 'string'
&& typeof (<URI>thing).fsPath === 'function'
&& typeof (<URI>thing).fsPath === 'string'
&& typeof (<URI>thing).with === 'function'
&& typeof (<URI>thing).toString === 'function';
}

View File

@ -189,10 +189,10 @@ function factory(nodeRequire, path, fs, perf) {
const initialLocale = locale;
perf.mark('nlsGeneration:start');
perf.mark('code/willGenerateNls');
const defaultResult = function (locale) {
perf.mark('nlsGeneration:end');
perf.mark('code/didGenerateNls');
return Promise.resolve({ locale: locale, availableLanguages: {} });
};
try {
@ -243,7 +243,7 @@ function factory(nodeRequire, path, fs, perf) {
if (fileExists) {
// We don't wait for this. No big harm if we can't touch
touch(coreLocation).catch(() => { });
perf.mark('nlsGeneration:end');
perf.mark('code/didGenerateNls');
return result;
}
return mkdirp(coreLocation).then(() => {
@ -282,7 +282,7 @@ function factory(nodeRequire, path, fs, perf) {
writes.push(writeFile(translationsConfigFile, JSON.stringify(packConfig.translations)));
return Promise.all(writes);
}).then(() => {
perf.mark('nlsGeneration:end');
perf.mark('code/didGenerateNls');
return result;
}).catch(err => {
console.error('Generating translation files failed.', err);

View File

@ -3,11 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import { tmpdir } from 'os';
import { join } from 'vs/base/common/path';
import { Queue } from 'vs/base/common/async';
import * as fs from 'fs';
import * as os from 'os';
import * as platform from 'vs/base/common/platform';
import { isMacintosh, isWindows } from 'vs/base/common/platform';
import { Event } from 'vs/base/common/event';
import { promisify } from 'util';
import { isRootOrDriveLetter } from 'vs/base/common/extpath';
@ -74,8 +74,8 @@ async function rimrafUnlink(path: string): Promise<void> {
// chmod as needed to allow for unlink
const mode = stat.mode;
if (!(mode & 128)) { // 128 === 0200
await chmod(path, mode | 128);
if (!(mode & fs.constants.S_IWUSR)) {
await chmod(path, mode | fs.constants.S_IWUSR);
}
return unlink(path);
@ -89,7 +89,7 @@ async function rimrafUnlink(path: string): Promise<void> {
async function rimrafMove(path: string): Promise<void> {
try {
const pathInTemp = join(os.tmpdir(), generateUuid());
const pathInTemp = join(tmpdir(), generateUuid());
try {
await rename(path, pathInTemp);
} catch (error) {
@ -129,8 +129,8 @@ export function rimrafSync(path: string): void {
// chmod as needed to allow for unlink
const mode = stat.mode;
if (!(mode & 128)) { // 128 === 0200
fs.chmodSync(path, mode | 128);
if (!(mode & fs.constants.S_IWUSR)) {
fs.chmodSync(path, mode | fs.constants.S_IWUSR);
}
return fs.unlinkSync(path);
@ -151,7 +151,7 @@ export async function readdirWithFileTypes(path: string): Promise<fs.Dirent[]> {
// Mac: uses NFD unicode form on disk, but we want NFC
// See also https://github.com/nodejs/node/issues/2165
if (platform.isMacintosh) {
if (isMacintosh) {
for (const child of children) {
child.name = normalizeNFC(child.name);
}
@ -167,7 +167,7 @@ export function readdirSync(path: string): string[] {
function handleDirectoryChildren(children: string[]): string[] {
// Mac: uses NFD unicode form on disk, but we want NFC
// See also https://github.com/nodejs/node/issues/2165
if (platform.isMacintosh) {
if (isMacintosh) {
return children.map(child => normalizeNFC(child));
}
@ -231,6 +231,25 @@ export async function statLink(path: string): Promise<IStatAndLink> {
return { stat: lstats, symbolicLink: { dangling: true } };
}
// Windows: workaround a node.js bug where reparse points
// are not supported (https://github.com/nodejs/node/issues/36790)
if (isWindows && error.code === 'EACCES' && lstats) {
try {
const stats = await stat(await readlink(path));
return { stat: stats, symbolicLink: lstats.isSymbolicLink() ? { dangling: false } : undefined };
} catch (error) {
// If the link points to a non-existing file we still want
// to return it as result while setting dangling: true flag
if (error.code === 'ENOENT') {
return { stat: lstats, symbolicLink: { dangling: true } };
}
throw error;
}
}
throw error;
}
}
@ -247,6 +266,10 @@ export function renameIgnoreError(oldPath: string, newPath: string): Promise<voi
return new Promise(resolve => fs.rename(oldPath, newPath, () => resolve()));
}
export function readlink(path: string): Promise<string> {
return promisify(fs.readlink)(path);
}
export function unlink(path: string): Promise<void> {
return promisify(fs.unlink)(path);
}
@ -290,7 +313,7 @@ export function writeFile(path: string, data: string | Buffer | Uint8Array, opti
function toQueueKey(path: string): string {
let queueKey = path;
if (platform.isWindows || platform.isMacintosh) {
if (isWindows || isMacintosh) {
queueKey = queueKey.toLowerCase(); // accommodate for case insensitive file systems
}
@ -394,11 +417,11 @@ export function writeFileSync(path: string, data: string | Buffer, options?: IWr
function ensureWriteOptions(options?: IWriteFileOptions): IEnsuredWriteFileOptions {
if (!options) {
return { mode: 0o666, flag: 'w' };
return { mode: 0o666 /* default node.js mode for files */, flag: 'w' };
}
return {
mode: typeof options.mode === 'number' ? options.mode : 0o666,
mode: typeof options.mode === 'number' ? options.mode : 0o666 /* default node.js mode for files */,
flag: typeof options.flag === 'string' ? options.flag : 'w'
};
}
@ -418,22 +441,26 @@ export async function readDirsInDir(dirPath: string): Promise<string[]> {
export async function dirExists(path: string): Promise<boolean> {
try {
const fileStat = await stat(path);
const { stat, symbolicLink } = await statLink(path);
return fileStat.isDirectory();
return stat.isDirectory() && symbolicLink?.dangling !== true;
} catch (error) {
return false;
// Ignore, path might not exist
}
return false;
}
export async function fileExists(path: string): Promise<boolean> {
try {
const fileStat = await stat(path);
const { stat, symbolicLink } = await statLink(path);
return fileStat.isFile();
return stat.isFile() && symbolicLink?.dangling !== true;
} catch (error) {
return false;
// Ignore, path might not exist
}
return false;
}
export function whenDeleted(path: string): Promise<void> {
@ -459,13 +486,13 @@ export function whenDeleted(path: string): Promise<void> {
export async function move(source: string, target: string): Promise<void> {
if (source === target) {
return Promise.resolve();
return;
}
async function updateMtime(path: string): Promise<void> {
const stat = await lstat(path);
if (stat.isDirectory() || stat.isSymbolicLink()) {
return Promise.resolve(); // only for files
return; // only for files
}
const fd = await promisify(fs.open)(path, 'a');
@ -501,28 +528,43 @@ export async function move(source: string, target: string): Promise<void> {
}
}
export async function copy(source: string, target: string, copiedSourcesIn?: { [path: string]: boolean }): Promise<void> {
const copiedSources = copiedSourcesIn ? copiedSourcesIn : Object.create(null);
// When copying a file or folder, we want to preserve the mode
// it had and as such provide it when creating. However, modes
// can go beyond what we expect (see link below), so we mask it.
// (https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588)
//
// The `copy` method is very old so we should probably revisit
// it's implementation and check wether this mask is still needed.
const COPY_MODE_MASK = 0o777;
const fileStat = await stat(source);
if (!fileStat.isDirectory()) {
return doCopyFile(source, target, fileStat.mode & 511);
export async function copy(source: string, target: string, handledSourcesIn?: { [path: string]: boolean }): Promise<void> {
// Keep track of paths already copied to prevent
// cycles from symbolic links to cause issues
const handledSources = handledSourcesIn ?? Object.create(null);
if (handledSources[source]) {
return;
} else {
handledSources[source] = true;
}
if (copiedSources[source]) {
return Promise.resolve(); // escape when there are cycles (can happen with symlinks)
const { stat, symbolicLink } = await statLink(source);
if (symbolicLink?.dangling) {
return; // skip over dangling symbolic links (https://github.com/microsoft/vscode/issues/111621)
}
copiedSources[source] = true; // remember as copied
if (!stat.isDirectory()) {
return doCopyFile(source, target, stat.mode & COPY_MODE_MASK);
}
// Create folder
await mkdirp(target, fileStat.mode & 511);
await mkdirp(target, stat.mode & COPY_MODE_MASK);
// Copy each file recursively
const files = await readdir(source);
for (let i = 0; i < files.length; i++) {
const file = files[i];
await copy(join(source, file), join(target, file), copiedSources);
await copy(join(source, file), join(target, file), handledSources);
}
}

View File

@ -0,0 +1,316 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as pfs from 'vs/base/node/pfs';
import * as os from 'os';
import * as path from 'vs/base/common/path';
import { env } from 'vs/base/common/process';
const WindowsPowerShell64BitLabel = 'Windows PowerShell';
const WindowsPowerShell32BitLabel = 'Windows PowerShell (x86)';
// This is required, since parseInt("7-preview") will return 7.
const IntRegex: RegExp = /^\d+$/;
const PwshMsixRegex: RegExp = /^Microsoft.PowerShell_.*/;
const PwshPreviewMsixRegex: RegExp = /^Microsoft.PowerShellPreview_.*/;
// The platform details descriptor for the platform we're on
const isProcess64Bit: boolean = process.arch === 'x64';
const isOS64Bit: boolean = isProcess64Bit || os.arch() === 'x64';
export interface IPowerShellExeDetails {
readonly displayName: string;
readonly exePath: string;
}
export interface IPossiblePowerShellExe extends IPowerShellExeDetails {
exists(): Promise<boolean>;
}
class PossiblePowerShellExe implements IPossiblePowerShellExe {
constructor(
public readonly exePath: string,
public readonly displayName: string,
private knownToExist?: boolean) { }
public async exists(): Promise<boolean> {
if (this.knownToExist === undefined) {
this.knownToExist = await pfs.fileExists(this.exePath);
}
return this.knownToExist;
}
}
function getProgramFilesPath(
{ useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): string | null {
if (!useAlternateBitness) {
// Just use the native system bitness
return env.ProgramFiles || null;
}
// We might be a 64-bit process looking for 32-bit program files
if (isProcess64Bit) {
return env['ProgramFiles(x86)'] || null;
}
// We might be a 32-bit process looking for 64-bit program files
if (isOS64Bit) {
return env.ProgramW6432 || null;
}
// We're a 32-bit process on 32-bit Windows, there is no other Program Files dir
return null;
}
function getSystem32Path({ useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): string {
const windir: string = env.windir!;
if (!useAlternateBitness) {
// Just use the native system bitness
return path.join(windir, 'System32');
}
// We might be a 64-bit process looking for 32-bit system32
if (isProcess64Bit) {
return path.join(windir, 'SysWOW64');
}
// We might be a 32-bit process looking for 64-bit system32
if (isOS64Bit) {
return path.join(windir, 'Sysnative');
}
// We're on a 32-bit Windows, so no alternate bitness
return path.join(windir, 'System32');
}
async function findPSCoreWindowsInstallation(
{ useAlternateBitness = false, findPreview = false }:
{ useAlternateBitness?: boolean; findPreview?: boolean } = {}): Promise<IPossiblePowerShellExe | null> {
const programFilesPath = getProgramFilesPath({ useAlternateBitness });
if (!programFilesPath) {
return null;
}
const powerShellInstallBaseDir = path.join(programFilesPath, 'PowerShell');
// Ensure the base directory exists
if (!await pfs.dirExists(powerShellInstallBaseDir)) {
return null;
}
let highestSeenVersion: number = -1;
let pwshExePath: string | null = null;
for (const item of await pfs.readdir(powerShellInstallBaseDir)) {
let currentVersion: number = -1;
if (findPreview) {
// We are looking for something like "7-preview"
// Preview dirs all have dashes in them
const dashIndex = item.indexOf('-');
if (dashIndex < 0) {
continue;
}
// Verify that the part before the dash is an integer
// and that the part after the dash is "preview"
const intPart: string = item.substring(0, dashIndex);
if (!IntRegex.test(intPart) || item.substring(dashIndex + 1) !== 'preview') {
continue;
}
currentVersion = parseInt(intPart, 10);
} else {
// Search for a directory like "6" or "7"
if (!IntRegex.test(item)) {
continue;
}
currentVersion = parseInt(item, 10);
}
// Ensure we haven't already seen a higher version
if (currentVersion <= highestSeenVersion) {
continue;
}
// Now look for the file
const exePath = path.join(powerShellInstallBaseDir, item, 'pwsh.exe');
if (!await pfs.fileExists(exePath)) {
continue;
}
pwshExePath = exePath;
highestSeenVersion = currentVersion;
}
if (!pwshExePath) {
return null;
}
const bitness: string = programFilesPath.includes('x86') ? ' (x86)' : '';
const preview: string = findPreview ? ' Preview' : '';
return new PossiblePowerShellExe(pwshExePath, `PowerShell${preview}${bitness}`, true);
}
async function findPSCoreMsix({ findPreview }: { findPreview?: boolean } = {}): Promise<IPossiblePowerShellExe | null> {
// We can't proceed if there's no LOCALAPPDATA path
if (!env.LOCALAPPDATA) {
return null;
}
// Find the base directory for MSIX application exe shortcuts
const msixAppDir = path.join(env.LOCALAPPDATA, 'Microsoft', 'WindowsApps');
if (!await pfs.dirExists(msixAppDir)) {
return null;
}
// Define whether we're looking for the preview or the stable
const { pwshMsixDirRegex, pwshMsixName } = findPreview
? { pwshMsixDirRegex: PwshPreviewMsixRegex, pwshMsixName: 'PowerShell Preview (Store)' }
: { pwshMsixDirRegex: PwshMsixRegex, pwshMsixName: 'PowerShell (Store)' };
// We should find only one such application, so return on the first one
for (const subdir of await pfs.readdir(msixAppDir)) {
if (pwshMsixDirRegex.test(subdir)) {
const pwshMsixPath = path.join(msixAppDir, subdir, 'pwsh.exe');
return new PossiblePowerShellExe(pwshMsixPath, pwshMsixName);
}
}
// If we find nothing, return null
return null;
}
function findPSCoreDotnetGlobalTool(): IPossiblePowerShellExe {
const dotnetGlobalToolExePath: string = path.join(os.homedir(), '.dotnet', 'tools', 'pwsh.exe');
return new PossiblePowerShellExe(dotnetGlobalToolExePath, '.NET Core PowerShell Global Tool');
}
function findWinPS({ useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): IPossiblePowerShellExe | null {
// x86 and ARM only have one WinPS on them
if (!isOS64Bit && useAlternateBitness) {
return null;
}
const systemFolderPath = getSystem32Path({ useAlternateBitness });
const winPSPath = path.join(systemFolderPath, 'WindowsPowerShell', 'v1.0', 'powershell.exe');
let displayName: string;
if (isProcess64Bit) {
displayName = useAlternateBitness
? WindowsPowerShell32BitLabel
: WindowsPowerShell64BitLabel;
} else if (isOS64Bit) {
displayName = useAlternateBitness
? WindowsPowerShell64BitLabel
: WindowsPowerShell32BitLabel;
} else {
// NOTE: ARM Windows devices also have Windows PowerShell x86 on them. There is no
// "ARM Windows PowerShell".
displayName = WindowsPowerShell32BitLabel;
}
return new PossiblePowerShellExe(winPSPath, displayName, true);
}
/**
* Iterates through all the possible well-known PowerShell installations on a machine.
* Returned values may not exist, but come with an .exists property
* which will check whether the executable exists.
*/
async function* enumerateDefaultPowerShellInstallations(): AsyncIterable<IPossiblePowerShellExe> {
// Find PSCore stable first
let pwshExe = await findPSCoreWindowsInstallation();
if (pwshExe) {
yield pwshExe;
}
// Windows may have a 32-bit pwsh.exe
pwshExe = await findPSCoreWindowsInstallation({ useAlternateBitness: true });
if (pwshExe) {
yield pwshExe;
}
// Also look for the MSIX/UWP installation
pwshExe = await findPSCoreMsix();
if (pwshExe) {
yield pwshExe;
}
// Look for the .NET global tool
// Some older versions of PowerShell have a bug in this where startup will fail,
// but this is fixed in newer versions
pwshExe = findPSCoreDotnetGlobalTool();
if (pwshExe) {
yield pwshExe;
}
// Look for PSCore preview
pwshExe = await findPSCoreWindowsInstallation({ findPreview: true });
if (pwshExe) {
yield pwshExe;
}
// Find a preview MSIX
pwshExe = await findPSCoreMsix({ findPreview: true });
if (pwshExe) {
yield pwshExe;
}
// Look for pwsh-preview with the opposite bitness
pwshExe = await findPSCoreWindowsInstallation({ useAlternateBitness: true, findPreview: true });
if (pwshExe) {
yield pwshExe;
}
// Finally, get Windows PowerShell
// Get the natural Windows PowerShell for the process bitness
pwshExe = findWinPS();
if (pwshExe) {
yield pwshExe;
}
// Get the alternate bitness Windows PowerShell
pwshExe = findWinPS({ useAlternateBitness: true });
if (pwshExe) {
yield pwshExe;
}
}
/**
* Iterates through PowerShell installations on the machine according
* to configuration passed in through the constructor.
* PowerShell items returned by this object are verified
* to exist on the filesystem.
*/
export async function* enumeratePowerShellInstallations(): AsyncIterable<IPowerShellExeDetails> {
// Get the default PowerShell installations first
for await (const defaultPwsh of enumerateDefaultPowerShellInstallations()) {
if (await defaultPwsh.exists()) {
yield defaultPwsh;
}
}
}
/**
* Returns the first available PowerShell executable found in the search order.
*/
export async function getFirstAvailablePowerShellInstallation(): Promise<IPowerShellExeDetails | null> {
for await (const pwsh of enumeratePowerShellInstallations()) {
return pwsh;
}
return null;
}

View File

@ -50,7 +50,7 @@ function terminateProcess(process: cp.ChildProcess, cwd?: string): Promise<Termi
options.cwd = cwd;
}
const killProcess = cp.execFile('taskkill', ['/T', '/F', '/PID', process.pid.toString()], options);
return new Promise((resolve, reject) => {
return new Promise(resolve => {
killProcess.once('error', (err) => {
resolve({ success: false, error: err });
});
@ -68,7 +68,7 @@ function terminateProcess(process: cp.ChildProcess, cwd?: string): Promise<Termi
} else if (Platform.isLinux || Platform.isMacintosh) {
try {
const cmd = FileAccess.asFileUri('vs/base/node/terminateProcess.sh', require).fsPath;
return new Promise((resolve, reject) => {
return new Promise(resolve => {
cp.execFile(cmd, [process.pid.toString()], { encoding: 'utf8', shell: true } as cp.ExecFileOptions, (err, stdout, stderr) => {
if (err) {
resolve({ success: false, error: err });
@ -86,8 +86,8 @@ function terminateProcess(process: cp.ChildProcess, cwd?: string): Promise<Termi
return Promise.resolve({ success: true });
}
export function getWindowsShell(environment: Platform.IProcessEnvironment = process.env as Platform.IProcessEnvironment): string {
return environment['comspec'] || 'cmd.exe';
export function getWindowsShell(env = process.env as Platform.IProcessEnvironment): string {
return env['comspec'] || 'cmd.exe';
}
export abstract class AbstractProcess<TProgressData> {
@ -447,8 +447,8 @@ export namespace win32 {
// to the current working directory.
return path.join(cwd, command);
}
if (paths === undefined && Types.isString(process.env.PATH)) {
paths = process.env.PATH.split(path.delimiter);
if (paths === undefined && Types.isString(process.env['PATH'])) {
paths = process.env['PATH'].split(path.delimiter);
}
// No PATH environment. Make path absolute to the cwd.
if (paths === undefined || paths.length === 0) {

View File

@ -0,0 +1,93 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import * as platform from 'vs/base/common/platform';
import { getFirstAvailablePowerShellInstallation } from 'vs/base/node/powershell';
import * as processes from 'vs/base/node/processes';
/**
* Gets the detected default shell for the _system_, not to be confused with VS Code's _default_
* shell that the terminal uses by default.
* @param p The platform to detect the shell of.
*/
export async function getSystemShell(p: platform.Platform, env = process.env as platform.IProcessEnvironment): Promise<string> {
if (p === platform.Platform.Windows) {
if (platform.isWindows) {
return getSystemShellWindows();
}
// Don't detect Windows shell when not on Windows
return processes.getWindowsShell(env);
}
return getSystemShellUnixLike(p, env);
}
export function getSystemShellSync(p: platform.Platform, env = process.env as platform.IProcessEnvironment): string {
if (p === platform.Platform.Windows) {
if (platform.isWindows) {
return getSystemShellWindowsSync(env);
}
// Don't detect Windows shell when not on Windows
return processes.getWindowsShell(env);
}
return getSystemShellUnixLike(p, env);
}
let _TERMINAL_DEFAULT_SHELL_UNIX_LIKE: string | null = null;
function getSystemShellUnixLike(p: platform.Platform, env: platform.IProcessEnvironment): string {
// Only use $SHELL for the current OS
if (platform.isLinux && p === platform.Platform.Mac || platform.isMacintosh && p === platform.Platform.Linux) {
return '/bin/bash';
}
if (!_TERMINAL_DEFAULT_SHELL_UNIX_LIKE) {
let unixLikeTerminal: string;
if (platform.isWindows) {
unixLikeTerminal = '/bin/bash'; // for WSL
} else {
unixLikeTerminal = env['SHELL'];
if (!unixLikeTerminal) {
try {
// It's possible for $SHELL to be unset, this API reads /etc/passwd. See https://github.com/github/codespaces/issues/1639
// Node docs: "Throws a SystemError if a user has no username or homedir."
unixLikeTerminal = os.userInfo().shell;
} catch (err) { }
}
if (!unixLikeTerminal) {
unixLikeTerminal = 'sh';
}
// Some systems have $SHELL set to /bin/false which breaks the terminal
if (unixLikeTerminal === '/bin/false') {
unixLikeTerminal = '/bin/bash';
}
}
_TERMINAL_DEFAULT_SHELL_UNIX_LIKE = unixLikeTerminal;
}
return _TERMINAL_DEFAULT_SHELL_UNIX_LIKE;
}
let _TERMINAL_DEFAULT_SHELL_WINDOWS: string | null = null;
async function getSystemShellWindows(): Promise<string> {
if (!_TERMINAL_DEFAULT_SHELL_WINDOWS) {
_TERMINAL_DEFAULT_SHELL_WINDOWS = (await getFirstAvailablePowerShellInstallation())!.exePath;
}
return _TERMINAL_DEFAULT_SHELL_WINDOWS;
}
function getSystemShellWindowsSync(env: platform.IProcessEnvironment): string {
if (_TERMINAL_DEFAULT_SHELL_WINDOWS) {
return _TERMINAL_DEFAULT_SHELL_WINDOWS;
}
const isAtLeastWindows10 = platform.isWindows && parseFloat(os.release()) >= 10;
const is32ProcessOn64Windows = env.hasOwnProperty('PROCESSOR_ARCHITEW6432');
const powerShellPath = `${env['windir']}\\${is32ProcessOn64Windows ? 'Sysnative' : 'System32'}\\WindowsPowerShell\\v1.0\\powershell.exe`;
return isAtLeastWindows10 ? powerShellPath : processes.getWindowsShell(env);
}

View File

@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable } from 'vs/base/common/lifecycle';
import { Client as MessagePortClient } from 'vs/base/parts/ipc/common/ipc.mp';
/**
* An implementation of a `IPCClient` on top of DOM `MessagePort`.
*/
export class Client extends MessagePortClient implements IDisposable {
/**
* @param clientId a way to uniquely identify this client among
* other clients. this is important for routing because every
* client can also be a server
*/
constructor(port: MessagePort, clientId: string) {
super(port, clientId);
}
}

View File

@ -11,6 +11,11 @@ export interface Sender {
send(channel: string, msg: unknown): void;
}
/**
* The Electron `Protocol` leverages Electron style IPC communication (`ipcRenderer`, `ipcMain`)
* for the implementation of the `IMessagePassingProtocol`. That style of API requires a channel
* name for sending data.
*/
export class Protocol implements IMessagePassingProtocol {
constructor(private sender: Sender, readonly onMessage: Event<VSBuffer>) { }
@ -23,7 +28,7 @@ export class Protocol implements IMessagePassingProtocol {
}
}
dispose(): void {
disconnect(): void {
this.sender.send('vscode:disconnect', null);
}
}

View 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 { Event } from 'vs/base/common/event';
import { IMessagePassingProtocol, IPCClient } from 'vs/base/parts/ipc/common/ipc';
import { IDisposable } from 'vs/base/common/lifecycle';
import { VSBuffer } from 'vs/base/common/buffer';
/**
* Declare minimal `MessageEvent` and `MessagePort` interfaces here
* so that this utility can be used both from `browser` and
* `electron-main` namespace where message ports are available.
*/
export interface MessageEvent {
/**
* For our use we only consider `Uint8Array` a valid data transfer
* via message ports because our protocol implementation is buffer based.
*/
data: Uint8Array;
}
export interface MessagePort {
addEventListener(type: 'message', listener: (this: MessagePort, e: MessageEvent) => unknown): void;
removeEventListener(type: 'message', listener: (this: MessagePort, e: MessageEvent) => unknown): void;
postMessage(message: Uint8Array): void;
start(): void;
close(): void;
}
/**
* The MessagePort `Protocol` leverages MessagePort style IPC communication
* for the implementation of the `IMessagePassingProtocol`. That style of API
* is a simple `onmessage` / `postMessage` pattern.
*/
export class Protocol implements IMessagePassingProtocol {
readonly onMessage = Event.fromDOMEventEmitter<VSBuffer>(this.port, 'message', (e: MessageEvent) => VSBuffer.wrap(e.data));
constructor(private port: MessagePort) {
// we must call start() to ensure messages are flowing
port.start();
}
send(message: VSBuffer): void {
this.port.postMessage(message.buffer);
}
disconnect(): void {
this.port.close();
}
}
/**
* An implementation of a `IPCClient` on top of MessagePort style IPC communication.
*/
export class Client extends IPCClient implements IDisposable {
private protocol: Protocol;
constructor(port: MessagePort, clientId: string) {
const protocol = new Protocol(port);
super(protocol, clientId);
this.protocol = protocol;
}
dispose(): void {
this.protocol.disconnect();
}
}

View File

@ -365,8 +365,8 @@ export class Protocol extends Disposable implements IMessagePassingProtocol {
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;
private readonly _onDidDispose = new Emitter<void>();
readonly onDidDispose: Event<void> = this._onDidDispose.event;
constructor(socket: ISocket) {
super();
@ -380,7 +380,7 @@ export class Protocol extends Disposable implements IMessagePassingProtocol {
}
}));
this._register(this._socket.onClose(() => this._onClose.fire()));
this._register(this._socket.onClose(() => this._onDidDispose.fire()));
}
drain(): Promise<void> {
@ -406,7 +406,7 @@ export class Client<TContext = string> extends IPCClient<TContext> {
return new Client(new Protocol(socket), id);
}
get onClose(): Event<void> { return this.protocol.onClose; }
get onDidDispose(): Event<void> { return this.protocol.onDidDispose; }
constructor(private protocol: Protocol | PersistentProtocol, id: TContext, ipcLogger: IIPCLogger | null = null) {
super(protocol, id, ipcLogger);
@ -621,8 +621,8 @@ export class PersistentProtocol implements IMessagePassingProtocol {
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 _onDidDispose = new BufferedEmitter<void>();
readonly onDidDispose: Event<void> = this._onDidDispose.event;
private readonly _onSocketClose = new BufferedEmitter<void>();
readonly onSocketClose: Event<void> = this._onSocketClose.event;
@ -747,6 +747,10 @@ export class PersistentProtocol implements IMessagePassingProtocol {
return this._socket;
}
public getMillisSinceLastIncomingData(): number {
return Date.now() - this._socketReader.lastReadTime;
}
public beginAcceptReconnection(socket: ISocket, initialDataChunk: VSBuffer | null): void {
this._isReconnecting = true;
@ -783,7 +787,7 @@ export class PersistentProtocol implements IMessagePassingProtocol {
}
public acceptDisconnect(): void {
this._onClose.fire();
this._onDidDispose.fire();
}
private _receiveMessage(msg: ProtocolMessage): void {
@ -820,7 +824,7 @@ export class PersistentProtocol implements IMessagePassingProtocol {
} else if (msg.type === ProtocolMessageType.Control) {
this._onControlMessage.fire(msg.data);
} else if (msg.type === ProtocolMessageType.Disconnect) {
this._onClose.fire();
this._onDidDispose.fire();
} else if (msg.type === ProtocolMessageType.ReplayRequest) {
// Send again all unacknowledged messages
const toSend = this._outgoingUnackMsg.toArray();

View File

@ -505,6 +505,7 @@ export interface IIPCLogger {
export class ChannelClient implements IChannelClient, IDisposable {
private isDisposed: boolean = false;
private state: State = State.Uninitialized;
private activeRequests = new Set<IDisposable>();
private handlers = new Map<number, IHandler>();
@ -525,9 +526,15 @@ export class ChannelClient implements IChannelClient, IDisposable {
return {
call(command: string, arg?: any, cancellationToken?: CancellationToken) {
if (that.isDisposed) {
return Promise.reject(errors.canceled());
}
return that.requestPromise(channelName, command, arg, cancellationToken);
},
listen(event: string, arg: any) {
if (that.isDisposed) {
return Promise.reject(errors.canceled());
}
return that.requestEvent(channelName, event, arg);
}
} as T;
@ -725,6 +732,7 @@ export class ChannelClient implements IChannelClient, IDisposable {
}
dispose(): void {
this.isDisposed = true;
if (this.protocolListener) {
this.protocolListener.dispose();
this.protocolListener = null;

View File

@ -3,10 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ipcMain, WebContents } from 'electron';
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 { Protocol as ElectronProtocol } from 'vs/base/parts/ipc/common/ipc.electron';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { VSBuffer } from 'vs/base/common/buffer';
@ -18,9 +18,13 @@ interface IIPCEvent {
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);
}
/**
* An implemention of `IPCServer` on top of Electron `ipcMain` API.
*/
export class Server extends IPCServer {
private static readonly Clients = new Map<number, IDisposable>();
@ -41,7 +45,7 @@ export class Server extends IPCServer {
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);
const protocol = new ElectronProtocol(webContents, onMessage);
return { protocol, onDidClientDisconnect };
});

View File

@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { BrowserWindow, ipcMain, IpcMainEvent, MessagePortMain } from 'electron';
import { Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { generateUuid } from 'vs/base/common/uuid';
import { Client as MessagePortClient } from 'vs/base/parts/ipc/common/ipc.mp';
/**
* An implementation of a `IPCClient` on top of Electron `MessagePortMain`.
*/
export class Client extends MessagePortClient implements IDisposable {
/**
* @param clientId a way to uniquely identify this client among
* other clients. this is important for routing because every
* client can also be a server
*/
constructor(port: MessagePortMain, clientId: string) {
super({
addEventListener: (type, listener) => port.addListener(type, listener),
removeEventListener: (type, listener) => port.removeListener(type, listener),
postMessage: message => port.postMessage(message),
start: () => port.start(),
close: () => port.close()
}, clientId);
}
}
/**
* This method opens a message channel connection
* in the target window. The target window needs
* to use the `Server` from `electron-sandbox/ipc.mp`.
*/
export async function connect(window: BrowserWindow): Promise<MessagePortMain> {
// Assert healthy window to talk to
if (window.isDestroyed() || window.webContents.isDestroyed()) {
throw new Error('ipc.mp#connect: Cannot talk to window because it is closed or destroyed');
}
// Ask to create message channel inside the window
// and send over a UUID to correlate the response
const nonce = generateUuid();
window.webContents.send('vscode:createMessageChannel', nonce);
// Wait until the window has returned the `MessagePort`
// We need to filter by the `nonce` to ensure we listen
// to the right response.
const onMessageChannelResult = Event.fromNodeEventEmitter<{ nonce: string, port: MessagePortMain }>(ipcMain, 'vscode:createMessageChannelResult', (e: IpcMainEvent, nonce: string) => ({ nonce, port: e.ports[0] }));
const { port } = await Event.toPromise(Event.once(Event.filter(onMessageChannelResult, e => e.nonce === nonce)));
return port;
}

View File

@ -5,28 +5,34 @@
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 { Protocol as ElectronProtocol } 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';
/**
* An implemention of `IPCClient` on top of Electron `ipcRenderer` IPC communication
* provided from sandbox globals (via preload script).
*/
export class Client extends IPCClient implements IDisposable {
private protocol: Protocol;
private protocol: ElectronProtocol;
private static createProtocol(): Protocol {
private static createProtocol(): ElectronProtocol {
const onMessage = Event.fromNodeEventEmitter<VSBuffer>(ipcRenderer, 'vscode:message', (_, message) => VSBuffer.wrap(message));
ipcRenderer.send('vscode:hello');
return new Protocol(ipcRenderer, onMessage);
return new ElectronProtocol(ipcRenderer, onMessage);
}
constructor(id: string) {
const protocol = Client.createProtocol();
super(protocol, id);
this.protocol = protocol;
}
dispose(): void {
this.protocol.dispose();
this.protocol.disconnect();
}
}

View File

@ -0,0 +1,51 @@
/*---------------------------------------------------------------------------------------------
* 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 { Event } from 'vs/base/common/event';
import { ClientConnectionEvent, IPCServer } from 'vs/base/parts/ipc/common/ipc';
import { Protocol as MessagePortProtocol } from 'vs/base/parts/ipc/common/ipc.mp';
/**
* An implementation of a `IPCServer` on top of MessagePort style IPC communication.
* The clients register themselves via Electron IPC transfer.
*/
export class Server extends IPCServer {
private static getOnDidClientConnect(): Event<ClientConnectionEvent> {
// Clients connect via `vscode:createMessageChannel` to get a
// `MessagePort` that is ready to be used. For every connection
// we create a pair of message ports and send it back.
//
// The `nonce` is included so that the main side has a chance to
// correlate the response back to the sender.
const onCreateMessageChannel = Event.fromNodeEventEmitter<string>(ipcRenderer, 'vscode:createMessageChannel', (_, nonce: string) => nonce);
return Event.map(onCreateMessageChannel, nonce => {
// Create a new pair of ports and protocol for this connection
const { port1: incomingPort, port2: outgoingPort } = new MessageChannel();
const protocol = new MessagePortProtocol(incomingPort);
const result: ClientConnectionEvent = {
protocol,
// Not part of the standard spec, but in Electron we get a `close` event
// when the other side closes. We can use this to detect disconnects
// (https://github.com/electron/electron/blob/11-x-y/docs/api/message-port-main.md#event-close)
onDidClientDisconnect: Event.fromDOMEventEmitter(incomingPort, 'close')
};
// Send one port back to the requestor
ipcRenderer.postMessage('vscode:createMessageChannelResult', nonce, [outgoingPort]);
return result;
});
}
constructor() {
super(Server.getOnDidClientConnect());
}
}

View File

@ -5,6 +5,7 @@
import { createHash } from 'crypto';
import { Socket, Server as NetServer, createConnection, createServer } from 'net';
import * as zlib from 'zlib';
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';
@ -120,24 +121,130 @@ const enum ReadState {
export class WebSocketNodeSocket extends Disposable implements ISocket {
public readonly socket: NodeSocket;
public readonly permessageDeflate: boolean;
private _totalIncomingWireBytes: number;
private _totalIncomingDataBytes: number;
private _totalOutgoingWireBytes: number;
private _totalOutgoingDataBytes: number;
private readonly _zlibInflate: zlib.InflateRaw | null;
private readonly _zlibDeflate: zlib.DeflateRaw | null;
private _zlibDeflateFlushWaitingCount: number;
private readonly _onDidZlibFlush = this._register(new Emitter<void>());
private readonly _recordInflateBytes: boolean;
private readonly _recordedInflateBytes: Buffer[] = [];
private readonly _pendingInflateData: Buffer[] = [];
private readonly _pendingDeflateData: Buffer[] = [];
private readonly _incomingData: ChunkStream;
private readonly _onData = this._register(new Emitter<VSBuffer>());
private readonly _onClose = this._register(new Emitter<void>());
private _isEnded: boolean = false;
private readonly _state = {
state: ReadState.PeekHeader,
readLen: Constants.MinHeaderByteSize,
fin: 0,
mask: 0
};
constructor(socket: NodeSocket) {
public get totalIncomingWireBytes(): number {
return this._totalIncomingWireBytes;
}
public get totalIncomingDataBytes(): number {
return this._totalIncomingDataBytes;
}
public get totalOutgoingWireBytes(): number {
return this._totalOutgoingWireBytes;
}
public get totalOutgoingDataBytes(): number {
return this._totalOutgoingDataBytes;
}
public get recordedInflateBytes(): VSBuffer {
if (this._recordInflateBytes) {
return VSBuffer.wrap(Buffer.concat(this._recordedInflateBytes));
}
return VSBuffer.alloc(0);
}
/**
* Create a socket which can communicate using WebSocket frames.
*
* **NOTE**: When using the permessage-deflate WebSocket extension, if parts of inflating was done
* in a different zlib instance, we need to pass all those bytes into zlib, otherwise the inflate
* might hit an inflated portion referencing a distance too far back.
*
* @param socket The underlying socket
* @param permessageDeflate Use the permessage-deflate WebSocket extension
* @param inflateBytes "Seed" zlib inflate with these bytes.
* @param recordInflateBytes Record all bytes sent to inflate
*/
constructor(socket: NodeSocket, permessageDeflate: boolean, inflateBytes: VSBuffer | null, recordInflateBytes: boolean) {
super();
this.socket = socket;
this._totalIncomingWireBytes = 0;
this._totalIncomingDataBytes = 0;
this._totalOutgoingWireBytes = 0;
this._totalOutgoingDataBytes = 0;
this.permessageDeflate = permessageDeflate;
this._recordInflateBytes = recordInflateBytes;
if (permessageDeflate) {
// See https://tools.ietf.org/html/rfc7692#page-16
// To simplify our logic, we don't negociate the window size
// and simply dedicate (2^15) / 32kb per web socket
this._zlibInflate = zlib.createInflateRaw({
windowBits: 15
});
this._zlibInflate.on('error', (err) => {
// zlib errors are fatal, since we have no idea how to recover
console.error(err);
onUnexpectedError(err);
this._onClose.fire();
});
this._zlibInflate.on('data', (data: Buffer) => {
this._pendingInflateData.push(data);
});
if (inflateBytes) {
this._zlibInflate.write(inflateBytes.buffer);
this._zlibInflate.flush(() => {
this._pendingInflateData.length = 0;
});
}
this._zlibDeflate = zlib.createDeflateRaw({
windowBits: 15
});
this._zlibDeflate.on('error', (err) => {
// zlib errors are fatal, since we have no idea how to recover
console.error(err);
onUnexpectedError(err);
this._onClose.fire();
});
this._zlibDeflate.on('data', (data: Buffer) => {
this._pendingDeflateData.push(data);
});
} else {
this._zlibInflate = null;
this._zlibDeflate = null;
}
this._zlibDeflateFlushWaitingCount = 0;
this._incomingData = new ChunkStream();
this._register(this.socket.onData(data => this._acceptChunk(data)));
this._register(this.socket.onClose(() => this._onClose.fire()));
}
public dispose(): void {
this.socket.dispose();
if (this._zlibDeflateFlushWaitingCount > 0) {
// Wait for any outstanding writes to finish before disposing
this._register(this._onDidZlibFlush.event(() => {
this.dispose();
}));
} else {
this.socket.dispose();
super.dispose();
}
}
public onData(listener: (e: VSBuffer) => void): IDisposable {
@ -145,7 +252,7 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
}
public onClose(listener: () => void): IDisposable {
return this.socket.onClose(listener);
return this._onClose.event(listener);
}
public onEnd(listener: () => void): IDisposable {
@ -153,6 +260,36 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
}
public write(buffer: VSBuffer): void {
this._totalOutgoingDataBytes += buffer.byteLength;
if (this._zlibDeflate) {
this._zlibDeflate.write(<Buffer>buffer.buffer);
this._zlibDeflateFlushWaitingCount++;
// See https://zlib.net/manual.html#Constants
this._zlibDeflate.flush(/*Z_SYNC_FLUSH*/2, () => {
this._zlibDeflateFlushWaitingCount--;
let data = Buffer.concat(this._pendingDeflateData);
this._pendingDeflateData.length = 0;
// See https://tools.ietf.org/html/rfc7692#section-7.2.1
data = data.slice(0, data.length - 4);
if (!this._isEnded) {
// Avoid ERR_STREAM_WRITE_AFTER_END
this._write(VSBuffer.wrap(data), true);
}
if (this._zlibDeflateFlushWaitingCount === 0) {
this._onDidZlibFlush.fire();
}
});
} else {
this._write(buffer, false);
}
}
private _write(buffer: VSBuffer, compressed: boolean): void {
let headerLen = Constants.MinHeaderByteSize;
if (buffer.byteLength < 126) {
headerLen += 0;
@ -163,7 +300,12 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
}
const header = VSBuffer.alloc(headerLen);
header.writeUInt8(0b10000010, 0);
if (compressed) {
// The RSV1 bit indicates a compressed frame
header.writeUInt8(0b11000010, 0);
} else {
header.writeUInt8(0b10000010, 0);
}
if (buffer.byteLength < 126) {
header.writeUInt8(buffer.byteLength, 1);
} else if (buffer.byteLength < 2 ** 16) {
@ -184,10 +326,12 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
header.writeUInt8((buffer.byteLength >>> 0) & 0b11111111, ++offset);
}
this._totalOutgoingWireBytes += header.byteLength + buffer.byteLength;
this.socket.write(VSBuffer.concat([header, buffer]));
}
public end(): void {
this._isEnded = true;
this.socket.end();
}
@ -195,6 +339,7 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
if (data.byteLength === 0) {
return;
}
this._totalIncomingWireBytes += data.byteLength;
this._incomingData.acceptChunk(data);
@ -203,14 +348,15 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
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 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.fin = finBit;
this._state.mask = 0;
} else if (this._state.state === ReadState.ReadHeader) {
@ -263,13 +409,37 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
this._state.readLen = Constants.MinHeaderByteSize;
this._state.mask = 0;
this._onData.fire(body);
if (this._zlibInflate) {
// See https://tools.ietf.org/html/rfc7692#section-7.2.2
if (this._recordInflateBytes) {
this._recordedInflateBytes.push(Buffer.from(<Buffer>body.buffer));
}
this._zlibInflate.write(<Buffer>body.buffer);
if (this._state.fin) {
if (this._recordInflateBytes) {
this._recordedInflateBytes.push(Buffer.from([0x00, 0x00, 0xff, 0xff]));
}
this._zlibInflate.write(Buffer.from([0x00, 0x00, 0xff, 0xff]));
}
this._zlibInflate.flush(() => {
const data = Buffer.concat(this._pendingInflateData);
this._pendingInflateData.length = 0;
this._totalIncomingDataBytes += data.length;
this._onData.fire(VSBuffer.wrap(data));
});
} else {
this._totalIncomingDataBytes += body.byteLength;
this._onData.fire(body);
}
}
}
}
public drain(): Promise<void> {
return this.socket.drain();
public async drain(): Promise<void> {
if (this._zlibDeflateFlushWaitingCount > 0) {
await Event.toPromise(this._onDidZlibFlush.event);
}
await this.socket.drain();
}
}

View File

@ -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 * as assert from 'assert';
import { Event } from 'vs/base/common/event';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Client as MessagePortClient } from 'vs/base/parts/ipc/browser/ipc.mp';
suite('IPC, MessagePorts', () => {
test('message passing', async () => {
const { port1, port2 } = new MessageChannel();
const client1 = new MessagePortClient(port1, 'client1');
const client2 = new MessagePortClient(port2, 'client2');
client1.registerChannel('client1', {
call(_: unknown, command: string, arg: any, cancellationToken: CancellationToken): Promise<any> {
switch (command) {
case 'testMethodClient1': return Promise.resolve('success1');
default: return Promise.reject(new Error('not implemented'));
}
},
listen(_: unknown, event: string, arg?: any): Event<any> {
switch (event) {
default: throw new Error('not implemented');
}
}
});
client2.registerChannel('client2', {
call(_: unknown, command: string, arg: any, cancellationToken: CancellationToken): Promise<any> {
switch (command) {
case 'testMethodClient2': return Promise.resolve('success2');
default: return Promise.reject(new Error('not implemented'));
}
},
listen(_: unknown, event: string, arg?: any): Event<any> {
switch (event) {
default: throw new Error('not implemented');
}
}
});
const channelClient1 = client2.getChannel('client1');
assert.strictEqual(await channelClient1.call('testMethodClient1'), 'success1');
const channelClient2 = client1.getChannel('client2');
assert.strictEqual(await channelClient2.call('testMethodClient2'), 'success2');
client1.dispose();
client2.dispose();
});
});

View File

@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* 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 as MessagePortClient } from 'vs/base/parts/ipc/browser/ipc.mp';
suite('IPC, MessagePorts', () => {
test('message port close event', async () => {
const { port1, port2 } = new MessageChannel();
new MessagePortClient(port1, 'client1');
const client2 = new MessagePortClient(port2, 'client2');
// This test ensures that Electron's API for the close event
// does not break because we rely on it to dispose client
// connections from the server.
//
// This event is not provided by browser MessagePort API though.
const whenClosed = new Promise<boolean>(resolve => port1.addEventListener('close', () => resolve(true)));
client2.dispose();
assert.ok(await whenClosed);
});
});

View File

@ -11,7 +11,7 @@ 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 }
env: { VSCODE_AMD_ENTRYPOINT: 'vs/base/parts/ipc/test/node/testApp', verbose: true }
});
}

View File

@ -135,13 +135,13 @@ suite('IPC, Socket Protocol', () => {
a.send(VSBuffer.fromString('foobarfarboo'));
const msg1 = await bMessages.waitForOne();
assert.equal(msg1.toString(), 'foobarfarboo');
assert.strictEqual(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);
assert.strictEqual(msg2.readUInt8(0), 123);
});
@ -160,7 +160,7 @@ suite('IPC, Socket Protocol', () => {
a.send(VSBuffer.fromString(JSON.stringify(data)));
const msg = await bMessages.waitForOne();
assert.deepEqual(JSON.parse(msg.toString()), data);
assert.deepStrictEqual(JSON.parse(msg.toString()), data);
});
});
@ -179,49 +179,49 @@ suite('PersistentProtocol reconnection', () => {
const bMessages = new MessageStream(b);
a.send(VSBuffer.fromString('a1'));
assert.equal(a.unacknowledgedCount, 1);
assert.equal(b.unacknowledgedCount, 0);
assert.strictEqual(a.unacknowledgedCount, 1);
assert.strictEqual(b.unacknowledgedCount, 0);
a.send(VSBuffer.fromString('a2'));
assert.equal(a.unacknowledgedCount, 2);
assert.equal(b.unacknowledgedCount, 0);
assert.strictEqual(a.unacknowledgedCount, 2);
assert.strictEqual(b.unacknowledgedCount, 0);
a.send(VSBuffer.fromString('a3'));
assert.equal(a.unacknowledgedCount, 3);
assert.equal(b.unacknowledgedCount, 0);
assert.strictEqual(a.unacknowledgedCount, 3);
assert.strictEqual(b.unacknowledgedCount, 0);
const a1 = await bMessages.waitForOne();
assert.equal(a1.toString(), 'a1');
assert.equal(a.unacknowledgedCount, 3);
assert.equal(b.unacknowledgedCount, 0);
assert.strictEqual(a1.toString(), 'a1');
assert.strictEqual(a.unacknowledgedCount, 3);
assert.strictEqual(b.unacknowledgedCount, 0);
const a2 = await bMessages.waitForOne();
assert.equal(a2.toString(), 'a2');
assert.equal(a.unacknowledgedCount, 3);
assert.equal(b.unacknowledgedCount, 0);
assert.strictEqual(a2.toString(), 'a2');
assert.strictEqual(a.unacknowledgedCount, 3);
assert.strictEqual(b.unacknowledgedCount, 0);
const a3 = await bMessages.waitForOne();
assert.equal(a3.toString(), 'a3');
assert.equal(a.unacknowledgedCount, 3);
assert.equal(b.unacknowledgedCount, 0);
assert.strictEqual(a3.toString(), 'a3');
assert.strictEqual(a.unacknowledgedCount, 3);
assert.strictEqual(b.unacknowledgedCount, 0);
b.send(VSBuffer.fromString('b1'));
assert.equal(a.unacknowledgedCount, 3);
assert.equal(b.unacknowledgedCount, 1);
assert.strictEqual(a.unacknowledgedCount, 3);
assert.strictEqual(b.unacknowledgedCount, 1);
const b1 = await aMessages.waitForOne();
assert.equal(b1.toString(), 'b1');
assert.equal(a.unacknowledgedCount, 0);
assert.equal(b.unacknowledgedCount, 1);
assert.strictEqual(b1.toString(), 'b1');
assert.strictEqual(a.unacknowledgedCount, 0);
assert.strictEqual(b.unacknowledgedCount, 1);
a.send(VSBuffer.fromString('a4'));
assert.equal(a.unacknowledgedCount, 1);
assert.equal(b.unacknowledgedCount, 1);
assert.strictEqual(a.unacknowledgedCount, 1);
assert.strictEqual(b.unacknowledgedCount, 1);
const b2 = await bMessages.waitForOne();
assert.equal(b2.toString(), 'a4');
assert.equal(a.unacknowledgedCount, 1);
assert.equal(b.unacknowledgedCount, 0);
assert.strictEqual(b2.toString(), 'a4');
assert.strictEqual(a.unacknowledgedCount, 1);
assert.strictEqual(b.unacknowledgedCount, 0);
});
});

View File

@ -121,7 +121,7 @@
font-size: 11px;
padding: 0 6px;
display: flex;
height: 100%;
height: 27.5px;
align-items: center;
}

View File

@ -30,7 +30,7 @@ import { Color } from 'vs/base/common/color';
import { registerCodicon, Codicon } from 'vs/base/common/codicons';
import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { escape } from 'vs/base/common/strings';
import { renderCodicons } from 'vs/base/browser/codicons';
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
export interface IQuickInputOptions {
idPrefix: string;
@ -72,7 +72,7 @@ const $ = dom.$;
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
const backButtonIcon = registerCodicon('quick-input-back', Codicon.arrowLeft, localize('backButtonIcon', 'Icon for the back button in the quick input dialog.'));
const backButtonIcon = registerCodicon('quick-input-back', Codicon.arrowLeft);
const backButton = {
iconClass: backButtonIcon.classNames,
@ -967,7 +967,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
const validationMessage = this.validationMessage || '';
if (this._lastValidationMessage !== validationMessage) {
this._lastValidationMessage = validationMessage;
dom.reset(this.ui.message, ...renderCodicons(escape(validationMessage)));
dom.reset(this.ui.message, ...renderLabelWithIcons(escape(validationMessage)));
this.showMessageDecoration(this.validationMessage ? Severity.Error : Severity.Ignore);
}
this.ui.customButton.label = this.customLabel || '';
@ -1103,7 +1103,7 @@ class InputBox extends QuickInput implements IInputBox {
const validationMessage = this.validationMessage || this.noValidationMessage;
if (this._lastValidationMessage !== validationMessage) {
this._lastValidationMessage = validationMessage;
dom.reset(this.ui.message, ...renderCodicons(validationMessage));
dom.reset(this.ui.message, ...renderLabelWithIcons(validationMessage));
this.showMessageDecoration(this.validationMessage ? Severity.Error : Severity.Ignore);
}
}

View File

@ -9,7 +9,7 @@ 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 { matchesFuzzyIconAware, parseLabelWithIcons } from 'vs/base/common/iconLabels';
import { compareAnything } from 'vs/base/common/comparers';
import { Emitter, Event } from 'vs/base/common/event';
import { KeyCode } from 'vs/base/common/keyCodes';
@ -34,6 +34,7 @@ interface IListElement {
readonly index: number;
readonly item: IQuickPickItem;
readonly saneLabel: string;
readonly saneMeta?: string;
readonly saneAriaLabel: string;
readonly saneDescription?: string;
readonly saneDetail?: string;
@ -49,6 +50,7 @@ class ListElement implements IListElement, IDisposable {
index!: number;
item!: IQuickPickItem;
saneLabel!: string;
saneMeta!: string;
saneAriaLabel!: string;
saneDescription?: string;
saneDetail?: string;
@ -127,7 +129,7 @@ class ListElementRenderer implements IListRenderer<ListElement, IListElementTemp
const row2 = dom.append(rows, $('.quick-input-list-row'));
// Label
data.label = new IconLabel(row1, { supportHighlights: true, supportDescriptionHighlights: true, supportCodicons: true });
data.label = new IconLabel(row1, { supportHighlights: true, supportDescriptionHighlights: true, supportIcons: true });
// Keybinding
const keybindingContainer = dom.append(row1, $('.quick-input-list-entry-keybinding'));
@ -190,12 +192,11 @@ class ListElementRenderer implements IListRenderer<ListElement, IListElementTemp
if (button.alwaysVisible) {
cssClasses = cssClasses ? `${cssClasses} always-visible` : 'always-visible';
}
const action = new Action(`id-${index}`, '', cssClasses, true, () => {
const action = new Action(`id-${index}`, '', cssClasses, true, async () => {
element.fireButtonTriggered({
button,
item: element.item
});
return Promise.resolve();
});
action.tooltip = button.tooltip || '';
return action;
@ -248,6 +249,7 @@ export class QuickInputList {
matchOnDescription = false;
matchOnDetail = false;
matchOnLabel = true;
matchOnMeta = true;
sortByLabel = true;
private readonly _onChangedAllVisibleChecked = new Emitter<boolean>();
onChangedAllVisibleChecked: Event<boolean> = this._onChangedAllVisibleChecked.event;
@ -421,10 +423,11 @@ export class QuickInputList {
if (item.type !== 'separator') {
const previous = index && inputElements[index - 1];
const saneLabel = item.label && item.label.replace(/\r?\n/g, ' ');
const saneMeta = item.meta && item.meta.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)
.map(s => s && parseLabelWithIcons(s).text)
.filter(s => !!s)
.join(', ');
@ -432,6 +435,7 @@ export class QuickInputList {
index,
item,
saneLabel,
saneMeta,
saneAriaLabel,
saneDescription,
saneDetail,
@ -597,14 +601,15 @@ export class QuickInputList {
});
}
// Filter by value (since we support codicons, use codicon aware fuzzy matching)
// Filter by value (since we support icons in labels, use $(..) 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;
const labelHighlights = this.matchOnLabel ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneLabel))) : undefined;
const descriptionHighlights = this.matchOnDescription ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneDescription || ''))) : undefined;
const detailHighlights = this.matchOnDetail ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneDetail || ''))) : undefined;
const metaHighlights = this.matchOnMeta ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneMeta || ''))) : undefined;
if (labelHighlights || descriptionHighlights || detailHighlights) {
if (labelHighlights || descriptionHighlights || detailHighlights || metaHighlights) {
element.labelHighlights = labelHighlights;
element.descriptionHighlights = descriptionHighlights;
element.detailHighlights = detailHighlights;

View File

@ -21,6 +21,7 @@ export interface IQuickPickItem {
type?: 'item';
id?: string;
label: string;
meta?: string;
ariaLabel?: string;
description?: string;
detail?: string;

View File

@ -7,7 +7,7 @@
// #######################################################################
// ### ###
// ### electron.d.ts types we need in a common layer for reuse ###
// ### (copied from Electron 9.x) ###
// ### (copied from Electron 11.x) ###
// ### ###
// #######################################################################
@ -212,7 +212,7 @@ export interface SaveDialogReturnValue {
export interface FileFilter {
// Docs: http://electronjs.org/docs/api/structures/file-filter
// Docs: https://electronjs.org/docs/api/structures/file-filter
extensions: string[];
name: string;
@ -220,7 +220,7 @@ export interface FileFilter {
export interface InputEvent {
// Docs: http://electronjs.org/docs/api/structures/input-event
// Docs: https://electronjs.org/docs/api/structures/input-event
/**
* An array of modifiers of the event, can be `shift`, `control`, `ctrl`, `alt`,
@ -232,7 +232,7 @@ export interface InputEvent {
export interface MouseInputEvent extends InputEvent {
// Docs: http://electronjs.org/docs/api/structures/mouse-input-event
// Docs: https://electronjs.org/docs/api/structures/mouse-input-event
/**
* The button pressed, can be `left`, `middle`, `right`.

View File

@ -35,6 +35,17 @@
}
},
/**
* @param {string} channel
* @param {any} message
* @param {MessagePort[]} transfer
*/
postMessage(channel, message, transfer) {
if (validateIPC(channel)) {
ipcRenderer.postMessage(channel, message, transfer);
}
},
/**
* @param {string} channel
* @param {any[]} args
@ -114,6 +125,7 @@
*/
process: {
get platform() { return process.platform; },
get arch() { return process.arch; },
get env() { return process.env; },
get versions() { return process.versions; },
get type() { return 'renderer'; },

View File

@ -7,35 +7,54 @@
// #######################################################################
// ### ###
// ### electron.d.ts types we expose from electron-sandbox ###
// ### (copied from Electron 9.x) ###
// ### (copied from Electron 11.x) ###
// ### ###
// #######################################################################
export interface IpcRendererEvent extends Event {
// Docs: https://electronjs.org/docs/api/structures/ipc-renderer-event
/**
* A list of MessagePorts that were transferred with this message
*/
ports: MessagePort[];
/**
* The `IpcRenderer` instance that emitted the event originally
*/
sender: IpcRenderer;
/**
* The `webContents.id` that sent the message, you can call
* `event.sender.sendTo(event.senderId, ...)` to reply to the message, see
* ipcRenderer.sendTo for more information. This only applies to messages sent from
* a different renderer. Messages sent directly from the main process set
* `event.senderId` to `0`.
*/
senderId: number;
}
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;
on(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): this;
/**
* 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;
once(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): this;
/**
* Removes the specified `listener` from the listener array for the specified
* `channel`.
*/
removeListener(channel: string, listener: (event: unknown, ...args: any[]) => void): void;
removeListener(channel: string, listener: (...args: any[]) => void): this;
/**
* 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.
* just like `window.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
@ -43,8 +62,51 @@ export interface IpcRenderer {
*
* The main process handles it by listening for `channel` with the `ipcMain`
* module.
*
* If you need to transfer a `MessagePort` to the main process, use
* `ipcRenderer.postMessage`.
*
* If you want to receive a single response from the main process, like the result
* of a method call, consider using `ipcRenderer.invoke`.
*/
send(channel: string, ...args: any[]): void;
/**
* Resolves with the response from the main process.
*
* Send a message to the main process via `channel` and expect a result
* asynchronously. Arguments will be serialized with the Structured Clone
* Algorithm, just like `window.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 should listen for `channel` with `ipcMain.handle()`.
*
* For example:
*
* If you need to transfer a `MessagePort` to the main process, use
* `ipcRenderer.postMessage`.
*
* If you do not need a response to the message, consider using `ipcRenderer.send`.
*/
invoke(channel: string, ...args: any[]): Promise<any>;
/**
* Send a message to the main process, optionally transferring ownership of zero or
* more `MessagePort` objects.
*
* The transferred `MessagePort` objects will be available in the main process as
* `MessagePortMain` objects by accessing the `ports` property of the emitted
* event.
*
* For example:
*
* For more information on using `MessagePort` and `MessageChannel`, see the MDN
* documentation.
*/
postMessage(channel: string, message: any, transfer?: MessagePort[]): void;
}
export interface WebFrame {
@ -70,16 +132,23 @@ export interface CrashReporter {
* 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.
* must be no longer than 39 bytes, and values must be no longer than 20320 bytes.
* Keys with names longer than the maximum will be silently ignored. Key values
* longer than the maximum length will be truncated.
*
* **Note:** On linux values that are longer than 127 bytes will be chunked into
* multiple keys, each 127 bytes in length. E.g. `addExtraParameter('foo',
* 'a'.repeat(130))` will result in two chunked keys `foo__1` and `foo__2`, the
* first will contain the first 127 bytes and the second will contain the remaining
* 3 bytes. On your crash reporting backend you should stitch together keys in
* this format.
*/
addExtraParameter(key: string, value: string): void;
}
export interface ProcessMemoryInfo {
// Docs: http://electronjs.org/docs/api/structures/process-memory-info
// Docs: https://electronjs.org/docs/api/structures/process-memory-info
/**
* The amount of memory not shared by other processes, such as JS heap or HTML
@ -133,10 +202,7 @@ export interface CrashReporterStartOptions {
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
* gzip`. Default is `false`.
*/
compress?: boolean;
/**

Some files were not shown because too many files have changed in this diff Show More