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:
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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) {
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
Binary file not shown.
@ -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';
|
||||
|
||||
|
@ -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':
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -20,4 +20,5 @@ export interface IHoverDelegateOptions {
|
||||
|
||||
export interface IHoverDelegate {
|
||||
showHover(options: IHoverDelegateOptions): IDisposable | undefined;
|
||||
hideHover(): void;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
|
@ -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) {
|
@ -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%;
|
||||
|
@ -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 } {
|
||||
|
@ -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 */
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
@ -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 = /(&)?(&)([^\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) {
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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' : ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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 } {
|
||||
|
@ -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;
|
||||
|
@ -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> {
|
||||
|
@ -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>> {
|
||||
|
@ -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: () => { }
|
||||
};
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
@ -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 || '');
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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}`;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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) => ' '.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;
|
||||
|
161
lib/vscode/src/vs/base/common/iconLabels.ts
Normal file
161
lib/vscode/src/vs/base/common/iconLabels.ts
Normal 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;
|
||||
}
|
@ -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.
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
12
lib/vscode/src/vs/base/common/performance.d.ts
vendored
12
lib/vscode/src/vs/base/common/performance.d.ts
vendored
@ -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[];
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -108,7 +108,6 @@ export function sanitizeProcessEnvironment(env: IProcessEnvironment, ...preserve
|
||||
}, {} as Record<string, boolean>);
|
||||
const keysToRemove = [
|
||||
/^ELECTRON_.+$/,
|
||||
/^GOOGLE_API_KEY$/,
|
||||
/^VSCODE_.+$/,
|
||||
/^SNAP(|_.*)$/,
|
||||
/^GDK_PIXBUF_.+$/,
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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';
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
316
lib/vscode/src/vs/base/node/powershell.ts
Normal file
316
lib/vscode/src/vs/base/node/powershell.ts
Normal 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;
|
||||
}
|
@ -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) {
|
||||
|
93
lib/vscode/src/vs/base/node/shell.ts
Normal file
93
lib/vscode/src/vs/base/node/shell.ts
Normal 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);
|
||||
}
|
22
lib/vscode/src/vs/base/parts/ipc/browser/ipc.mp.ts
Normal file
22
lib/vscode/src/vs/base/parts/ipc/browser/ipc.mp.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
78
lib/vscode/src/vs/base/parts/ipc/common/ipc.mp.ts
Normal file
78
lib/vscode/src/vs/base/parts/ipc/common/ipc.mp.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { 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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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 };
|
||||
});
|
57
lib/vscode/src/vs/base/parts/ipc/electron-main/ipc.mp.ts
Normal file
57
lib/vscode/src/vs/base/parts/ipc/electron-main/ipc.mp.ts
Normal 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;
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
51
lib/vscode/src/vs/base/parts/ipc/electron-sandbox/ipc.mp.ts
Normal file
51
lib/vscode/src/vs/base/parts/ipc/electron-sandbox/ipc.mp.ts
Normal 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());
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
58
lib/vscode/src/vs/base/parts/ipc/test/browser/ipc.mp.test.ts
Normal file
58
lib/vscode/src/vs/base/parts/ipc/test/browser/ipc.mp.test.ts
Normal 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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
@ -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 }
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -121,7 +121,7 @@
|
||||
font-size: 11px;
|
||||
padding: 0 6px;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
height: 27.5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -21,6 +21,7 @@ export interface IQuickPickItem {
|
||||
type?: 'item';
|
||||
id?: string;
|
||||
label: string;
|
||||
meta?: string;
|
||||
ariaLabel?: string;
|
||||
description?: string;
|
||||
detail?: string;
|
||||
|
@ -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`.
|
||||
|
@ -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'; },
|
||||
|
@ -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
Reference in New Issue
Block a user