mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-30 17:23:29 -05:00
Merge from vscode e3c4990c67c40213af168300d1cfeb71d680f877 (#16569)
This commit is contained in:
@@ -28,7 +28,7 @@ class WindowManager {
|
||||
}
|
||||
|
||||
this._zoomLevel = zoomLevel;
|
||||
// See https://github.com/Microsoft/vscode/issues/26151
|
||||
// See https://github.com/microsoft/vscode/issues/26151
|
||||
this._lastZoomLevelChangeTime = isTrusted ? 0 : Date.now();
|
||||
this._onDidChangeZoomLevel.fire(this._zoomLevel);
|
||||
}
|
||||
@@ -115,7 +115,6 @@ 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 isEdgeLegacyWebView = (userAgent.indexOf('Edge/') >= 0) && (userAgent.indexOf('WebView/') >= 0);
|
||||
export const isElectron = (userAgent.indexOf('Electron/') >= 0);
|
||||
export const isAndroid = (userAgent.indexOf('Android') >= 0);
|
||||
|
||||
@@ -17,6 +17,7 @@ import { FileAccess, RemoteAuthorities } from 'vs/base/common/network';
|
||||
import { BrowserFeatures } from 'vs/base/browser/canIUse';
|
||||
import { insane, InsaneOptions } from 'vs/base/common/insane/insane';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
|
||||
export function clearNode(node: HTMLElement): void {
|
||||
while (node.firstChild) {
|
||||
@@ -25,79 +26,12 @@ export function clearNode(node: HTMLElement): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `node.remove()` instead
|
||||
* @deprecated Use node.isConnected directly
|
||||
*/
|
||||
export function removeNode(node: HTMLElement): void {
|
||||
if (node.parentNode) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
export function trustedInnerHTML(node: Element, value: TrustedHTML): void {
|
||||
// this is a workaround for innerHTML not allowing for "asymetric" accessors
|
||||
// see https://github.com/microsoft/vscode/issues/106396#issuecomment-692625393
|
||||
// and https://github.com/microsoft/TypeScript/issues/30024
|
||||
node.innerHTML = value as unknown as string;
|
||||
}
|
||||
|
||||
export function isInDOM(node: Node | null): boolean {
|
||||
return node?.isConnected ?? false;
|
||||
}
|
||||
|
||||
interface IDomClassList {
|
||||
hasClass(node: HTMLElement | SVGElement, className: string): boolean;
|
||||
addClass(node: HTMLElement | SVGElement, className: string): void;
|
||||
addClasses(node: HTMLElement | SVGElement, ...classNames: string[]): void;
|
||||
removeClass(node: HTMLElement | SVGElement, className: string): void;
|
||||
removeClasses(node: HTMLElement | SVGElement, ...classNames: string[]): void;
|
||||
toggleClass(node: HTMLElement | SVGElement, className: string, shouldHaveIt?: boolean): void;
|
||||
}
|
||||
|
||||
const _classList: IDomClassList = new class implements IDomClassList {
|
||||
hasClass(node: HTMLElement, className: string): boolean {
|
||||
return Boolean(className) && node.classList && node.classList.contains(className);
|
||||
}
|
||||
|
||||
addClasses(node: HTMLElement, ...classNames: string[]): void {
|
||||
classNames.forEach(nameValue => nameValue.split(' ').forEach(name => this.addClass(node, name)));
|
||||
}
|
||||
|
||||
addClass(node: HTMLElement, className: string): void {
|
||||
if (className && node.classList) {
|
||||
node.classList.add(className);
|
||||
}
|
||||
}
|
||||
|
||||
removeClass(node: HTMLElement, className: string): void {
|
||||
if (className && node.classList) {
|
||||
node.classList.remove(className);
|
||||
}
|
||||
}
|
||||
|
||||
removeClasses(node: HTMLElement, ...classNames: string[]): void {
|
||||
classNames.forEach(nameValue => nameValue.split(' ').forEach(name => this.removeClass(node, name)));
|
||||
}
|
||||
|
||||
toggleClass(node: HTMLElement, className: string, shouldHaveIt?: boolean): void {
|
||||
if (node.classList) {
|
||||
node.classList.toggle(className, shouldHaveIt);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** @deprecated ES6 - use classList*/
|
||||
export function hasClass(node: HTMLElement | SVGElement, className: string): boolean { return _classList.hasClass(node, className); }
|
||||
/** @deprecated ES6 - use classList*/
|
||||
export function addClass(node: HTMLElement | SVGElement, className: string): void { return _classList.addClass(node, className); }
|
||||
/** @deprecated ES6 - use classList*/
|
||||
export function addClasses(node: HTMLElement | SVGElement, ...classNames: string[]): void { return _classList.addClasses(node, ...classNames); }
|
||||
/** @deprecated ES6 - use classList*/
|
||||
export function removeClass(node: HTMLElement | SVGElement, className: string): void { return _classList.removeClass(node, className); }
|
||||
/** @deprecated ES6 - use classList*/
|
||||
export function removeClasses(node: HTMLElement | SVGElement, ...classNames: string[]): void { return _classList.removeClasses(node, ...classNames); }
|
||||
/** @deprecated ES6 - use classList*/
|
||||
export function toggleClass(node: HTMLElement | SVGElement, className: string, shouldHaveIt?: boolean): void { return _classList.toggleClass(node, className, shouldHaveIt); }
|
||||
|
||||
class DomListener implements IDisposable {
|
||||
|
||||
private _handler: (e: any) => void;
|
||||
@@ -414,16 +348,7 @@ export function getClientArea(element: HTMLElement): Dimension {
|
||||
|
||||
// If visual view port exits and it's on mobile, it should be used instead of window innerWidth / innerHeight, or document.body.clientWidth / document.body.clientHeight
|
||||
if (platform.isIOS && window.visualViewport) {
|
||||
const width = window.visualViewport.width;
|
||||
const height = window.visualViewport.height - (
|
||||
browser.isStandalone
|
||||
// in PWA mode, the visual viewport always includes the safe-area-inset-bottom (which is for the home indicator)
|
||||
// even when you are using the onscreen monitor, the visual viewport will include the area between system statusbar and the onscreen keyboard
|
||||
// plus the area between onscreen keyboard and the bottom bezel, which is 20px on iOS.
|
||||
? (20 + 4) // + 4px for body margin
|
||||
: 0
|
||||
);
|
||||
return new Dimension(width, height);
|
||||
return new Dimension(window.visualViewport.width, window.visualViewport.height);
|
||||
}
|
||||
|
||||
// Try innerWidth / innerHeight
|
||||
@@ -1254,28 +1179,44 @@ export function computeScreenAwareSize(cssPx: number): number {
|
||||
}
|
||||
|
||||
/**
|
||||
* Open safely a new window. This is the best way to do so, but you cannot tell
|
||||
* if the window was opened or if it was blocked by the brower's popup blocker.
|
||||
* If you want to tell if the browser blocked the new window, use `windowOpenNoOpenerWithSuccess`.
|
||||
*
|
||||
* See https://github.com/microsoft/monaco-editor/issues/601
|
||||
* To protect against malicious code in the linked site, particularly phishing attempts,
|
||||
* the window.opener should be set to null to prevent the linked site from having access
|
||||
* to change the location of the current page.
|
||||
* See https://mathiasbynens.github.io/rel-noopener/
|
||||
*/
|
||||
export function windowOpenNoOpener(url: string): boolean {
|
||||
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);
|
||||
export function windowOpenNoOpener(url: string): void {
|
||||
// By using 'noopener' in the `windowFeatures` argument, the newly created window will
|
||||
// not be able to use `window.opener` to reach back to the current page.
|
||||
// See https://stackoverflow.com/a/46958731
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/Window/open#noopener
|
||||
// However, this also doesn't allow us to realize if the browser blocked
|
||||
// the creation of the window.
|
||||
window.open(url, '_blank', 'noopener');
|
||||
}
|
||||
|
||||
/**
|
||||
* Open safely a new window. This technique is not appropiate in certain contexts,
|
||||
* like for example when the JS context is executing inside a sandboxed iframe.
|
||||
* If it is not necessary to know if the browser blocked the new window, use
|
||||
* `windowOpenNoOpener`.
|
||||
*
|
||||
* See https://github.com/microsoft/monaco-editor/issues/601
|
||||
* See https://github.com/microsoft/monaco-editor/issues/2474
|
||||
* See https://mathiasbynens.github.io/rel-noopener/
|
||||
*/
|
||||
export function windowOpenNoOpenerWithSuccess(url: string): boolean {
|
||||
const newTab = window.open();
|
||||
if (newTab) {
|
||||
(newTab as any).opener = null;
|
||||
newTab.location.href = url;
|
||||
return true;
|
||||
} else {
|
||||
let newTab = window.open();
|
||||
if (newTab) {
|
||||
(newTab as any).opener = null;
|
||||
newTab.location.href = url;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function animate(fn: () => void): IDisposable {
|
||||
@@ -1333,6 +1274,29 @@ export function triggerDownload(dataOrUri: Uint8Array | URI, name: string): void
|
||||
setTimeout(() => document.body.removeChild(anchor));
|
||||
}
|
||||
|
||||
export function triggerUpload(): Promise<FileList | undefined> {
|
||||
return new Promise<FileList | undefined>(resolve => {
|
||||
|
||||
// In order to upload to the browser, create a
|
||||
// input element of type `file` and click it
|
||||
// to gather the selected files
|
||||
const input = document.createElement('input');
|
||||
document.body.appendChild(input);
|
||||
input.type = 'file';
|
||||
input.multiple = true;
|
||||
|
||||
// Resolve once the input event has fired once
|
||||
Event.once(Event.fromDOMEventEmitter(input, 'input'))(() => {
|
||||
resolve(withNullAsUndefined(input.files));
|
||||
});
|
||||
|
||||
input.click();
|
||||
|
||||
// Ensure to remove the element from DOM eventually
|
||||
setTimeout(() => document.body.removeChild(input));
|
||||
});
|
||||
}
|
||||
|
||||
export enum DetectedFullscreenMode {
|
||||
|
||||
/**
|
||||
@@ -1518,6 +1482,9 @@ export class ModifierKeyEmitter extends Emitter<IModifierKeyStatus> {
|
||||
};
|
||||
|
||||
this._subscriptions.add(domEvent(window, 'keydown', true)(e => {
|
||||
if (e.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
// If Alt-key keydown event is repeated, ignore it #112347
|
||||
@@ -1552,6 +1519,10 @@ export class ModifierKeyEmitter extends Emitter<IModifierKeyStatus> {
|
||||
}));
|
||||
|
||||
this._subscriptions.add(domEvent(window, 'keyup', true)(e => {
|
||||
if (e.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.altKey && this._keyStatus.altKey) {
|
||||
this._keyStatus.lastKeyReleased = 'alt';
|
||||
} else if (!e.ctrlKey && this._keyStatus.ctrlKey) {
|
||||
@@ -1642,3 +1613,13 @@ export function getCookieValue(name: string): string | undefined {
|
||||
|
||||
return match ? match.pop() : undefined;
|
||||
}
|
||||
|
||||
export function addMatchMediaChangeListener(query: string, callback: () => void): void {
|
||||
const mediaQueryList = window.matchMedia(query);
|
||||
if (typeof mediaQueryList.addEventListener === 'function') {
|
||||
mediaQueryList.addEventListener('change', callback);
|
||||
} else {
|
||||
// Safari 13.x
|
||||
mediaQueryList.addListener(callback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { GestureEvent } from 'vs/base/browser/touch';
|
||||
import { Event as BaseEvent, Emitter } from 'vs/base/common/event';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export type EventHandler = HTMLElement | HTMLDocument | Window;
|
||||
|
||||
@@ -12,6 +14,9 @@ export interface IDomEvent {
|
||||
(element: EventHandler, type: string, useCapture?: boolean): BaseEvent<unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `DomEmitter` instead
|
||||
*/
|
||||
export const domEvent: IDomEvent = (element: EventHandler, type: string, useCapture?: boolean) => {
|
||||
const fn = (e: Event) => emitter.fire(e);
|
||||
const emitter = new Emitter<Event>({
|
||||
@@ -26,6 +31,35 @@ export const domEvent: IDomEvent = (element: EventHandler, type: string, useCapt
|
||||
return emitter.event;
|
||||
};
|
||||
|
||||
export interface DOMEventMap extends HTMLElementEventMap {
|
||||
'-monaco-gesturetap': GestureEvent;
|
||||
'-monaco-gesturechange': GestureEvent;
|
||||
'-monaco-gesturestart': GestureEvent;
|
||||
'-monaco-gesturesend': GestureEvent;
|
||||
'-monaco-gesturecontextmenu': GestureEvent;
|
||||
}
|
||||
|
||||
export class DomEmitter<K extends keyof DOMEventMap> implements IDisposable {
|
||||
|
||||
private emitter: Emitter<DOMEventMap[K]>;
|
||||
|
||||
get event(): BaseEvent<DOMEventMap[K]> {
|
||||
return this.emitter.event;
|
||||
}
|
||||
|
||||
constructor(element: EventHandler, type: K, useCapture?: boolean) {
|
||||
const fn = (e: Event) => this.emitter.fire(e as DOMEventMap[K]);
|
||||
this.emitter = new Emitter({
|
||||
onFirstListenerAdd: () => element.addEventListener(type, fn, useCapture),
|
||||
onLastListenerRemove: () => element.removeEventListener(type, fn, useCapture)
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.emitter.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export interface CancellableEvent {
|
||||
preventDefault(): void;
|
||||
stopPropagation(): void;
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as dom from 'vs/base/browser/dom';
|
||||
import { IframeUtils } from 'vs/base/browser/iframe';
|
||||
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { isIOS } from 'vs/base/common/platform';
|
||||
|
||||
export interface IStandardMouseMoveEventData {
|
||||
leftButton: boolean;
|
||||
@@ -88,7 +89,7 @@ export class GlobalMouseMoveMonitor<R extends { buttons: number; }> implements I
|
||||
this._onStopCallback = onStopCallback;
|
||||
|
||||
const windowChain = IframeUtils.getSameOriginWindowChain();
|
||||
const mouseMove = 'mousemove';
|
||||
const mouseMove = isIOS ? 'pointermove' : 'mousemove'; // Safari sends wrong event, workaround for #122653
|
||||
const mouseUp = 'mouseup';
|
||||
|
||||
const listenTo: (Document | ShadowRoot)[] = windowChain.map(element => element.window.document);
|
||||
|
||||
@@ -9,4 +9,4 @@ export interface IHistoryNavigationWidget {
|
||||
|
||||
showNextValue(): void;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,18 +43,6 @@ function getParentWindowIfSameOrigin(w: Window): Window | null {
|
||||
return w.parent;
|
||||
}
|
||||
|
||||
function findIframeElementInParentWindow(parentWindow: Window, childWindow: Window): HTMLIFrameElement | null {
|
||||
let parentWindowIframes = parentWindow.document.getElementsByTagName('iframe');
|
||||
let iframe: HTMLIFrameElement;
|
||||
for (let i = 0, len = parentWindowIframes.length; i < len; i++) {
|
||||
iframe = parentWindowIframes[i];
|
||||
if (iframe.contentWindow === childWindow) {
|
||||
return iframe;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export class IframeUtils {
|
||||
|
||||
/**
|
||||
@@ -72,7 +60,7 @@ export class IframeUtils {
|
||||
if (parent) {
|
||||
sameOriginWindowChainCache.push({
|
||||
window: w,
|
||||
iframeElement: findIframeElementInParentWindow(parent, w)
|
||||
iframeElement: w.frameElement || null
|
||||
});
|
||||
} else {
|
||||
sameOriginWindowChainCache.push({
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as DOM from 'vs/base/browser/dom'; // {{SQL CARBON EDIT}} added missing import to fix build break
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { createElement, FormattedTextRenderOptions } from 'vs/base/browser/formattedTextRenderer';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { IMarkdownString, parseHrefAndDimensions, removeMarkdownEscapes } from 'vs/base/common/htmlContent';
|
||||
@@ -180,15 +180,6 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
|
||||
});
|
||||
});
|
||||
|
||||
// const promise = Promise.all([value, withInnerHTML]).then(values => {
|
||||
// const span = <HTMLDivElement>element.querySelector(`div[data-code="${id}"]`);
|
||||
// if (span) {
|
||||
// DOM.reset(span, values[0]);
|
||||
// }
|
||||
// }).catch(_err => {
|
||||
// // ignore
|
||||
// });
|
||||
|
||||
if (options.asyncRenderCallback) {
|
||||
promise.then(options.asyncRenderCallback);
|
||||
}
|
||||
@@ -324,6 +315,14 @@ function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOpti
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips all markdown from `string`, if it's an IMarkdownString. For example
|
||||
* `# Header` would be output as `Header`. If it's not, the string is returned.
|
||||
*/
|
||||
export function renderStringAsPlaintext(string: IMarkdownString | string) {
|
||||
return typeof string === 'string' ? string : renderMarkdownAsPlaintext(string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips all markdown from `markdown`. For example `# Header` would be output as `Header`.
|
||||
*/
|
||||
@@ -398,6 +397,7 @@ export function renderMarkdownAsPlaintext(markdown: IMarkdownString) {
|
||||
|
||||
const unescapeInfo = new Map<string, string>([
|
||||
['"', '"'],
|
||||
[' ', ' '],
|
||||
['&', '&'],
|
||||
[''', '\''],
|
||||
['<', '<'],
|
||||
|
||||
@@ -75,6 +75,16 @@
|
||||
margin-right: .8em;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item .action-label.separator {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
margin: 5px 4px !important;
|
||||
cursor: default;
|
||||
min-width: 1px;
|
||||
padding: 0;
|
||||
background-color: #bbb;
|
||||
}
|
||||
|
||||
.secondary-actions .monaco-action-bar .action-label {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
.monaco-aria-container {
|
||||
position: absolute; /* try to hide from window but not from screen readers */
|
||||
left:-999em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
|
||||
.monaco-button-dropdown {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-button-dropdown > .monaco-dropdown-button {
|
||||
|
||||
@@ -185,6 +185,7 @@ export class Button extends Disposable implements IButton {
|
||||
this.buttonSecondaryHoverBackground = styles.buttonSecondaryHoverBackground;
|
||||
this.buttonBorder = styles.buttonBorder;
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
this.buttonSecondaryBorder = styles.buttonSecondaryBorder;
|
||||
this.buttonDisabledBackground = styles.buttonDisabledBackground;
|
||||
this.buttonDisabledForeground = styles.buttonDisabledForeground;
|
||||
|
||||
@@ -165,6 +165,7 @@ export class CenteredViewLayout implements IDisposable {
|
||||
this.splitView = undefined;
|
||||
this.emptyViews = undefined;
|
||||
this.container.appendChild(this.view.element);
|
||||
this.view.layout(this.width, this.height, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -87,7 +87,7 @@ export class Dialog extends Disposable {
|
||||
private readonly inputs: InputBox[];
|
||||
private readonly buttons: string[];
|
||||
|
||||
constructor(private container: HTMLElement, private message: string, buttons: string[], private options: IDialogOptions) {
|
||||
constructor(private container: HTMLElement, private message: string, buttons: string[] | undefined, private options: IDialogOptions) {
|
||||
super();
|
||||
|
||||
this.modalElement = this.container.appendChild($(`.monaco-dialog-modal-block.dimmed`));
|
||||
@@ -96,21 +96,22 @@ export class Dialog extends Disposable {
|
||||
this.element.setAttribute('role', 'dialog');
|
||||
hide(this.element);
|
||||
|
||||
this.buttons = buttons.length ? buttons : [nls.localize('ok', "OK")]; // If no button is provided, default to OK
|
||||
this.buttons = Array.isArray(buttons) && buttons.length ? buttons : [nls.localize('ok', "OK")]; // If no button is provided, default to OK
|
||||
const buttonsRowElement = this.element.appendChild($('.dialog-buttons-row'));
|
||||
this.buttonsContainer = buttonsRowElement.appendChild($('.dialog-buttons'));
|
||||
|
||||
const messageRowElement = this.element.appendChild($('.dialog-message-row'));
|
||||
this.iconElement = messageRowElement.appendChild($('.dialog-icon'));
|
||||
this.iconElement = messageRowElement.appendChild($('#monaco-dialog-icon.dialog-icon'));
|
||||
this.iconElement.setAttribute('aria-label', this.getIconAriaLabel());
|
||||
this.messageContainer = messageRowElement.appendChild($('.dialog-message-container'));
|
||||
|
||||
if (this.options.detail || this.options.renderBody) {
|
||||
const messageElement = this.messageContainer.appendChild($('.dialog-message'));
|
||||
const messageTextElement = messageElement.appendChild($('.dialog-message-text'));
|
||||
const messageTextElement = messageElement.appendChild($('#monaco-dialog-message-text.dialog-message-text'));
|
||||
messageTextElement.innerText = this.message;
|
||||
}
|
||||
|
||||
this.messageDetailElement = this.messageContainer.appendChild($('.dialog-message-detail'));
|
||||
this.messageDetailElement = this.messageContainer.appendChild($('#monaco-dialog-message-detail.dialog-message-detail'));
|
||||
if (this.options.detail || !this.options.renderBody) {
|
||||
this.messageDetailElement.innerText = this.options.detail ? this.options.detail : message;
|
||||
} else {
|
||||
@@ -118,8 +119,12 @@ export class Dialog extends Disposable {
|
||||
}
|
||||
|
||||
if (this.options.renderBody) {
|
||||
const customBody = this.messageContainer.appendChild($('.dialog-message-body'));
|
||||
const customBody = this.messageContainer.appendChild($('#monaco-dialog-message-body.dialog-message-body'));
|
||||
this.options.renderBody(customBody);
|
||||
|
||||
for (const el of this.messageContainer.querySelectorAll('a')) {
|
||||
el.tabIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.inputs) {
|
||||
@@ -157,7 +162,7 @@ export class Dialog extends Disposable {
|
||||
this.toolbarContainer = toolbarRowElement.appendChild($('.dialog-toolbar'));
|
||||
}
|
||||
|
||||
private getAriaLabel(): string {
|
||||
private getIconAriaLabel(): string {
|
||||
let typeLabel = nls.localize('dialogInfoMessage', 'Info');
|
||||
switch (this.options.type) {
|
||||
case 'error':
|
||||
@@ -176,7 +181,7 @@ export class Dialog extends Disposable {
|
||||
break;
|
||||
}
|
||||
|
||||
return `${typeLabel}: ${this.message} ${this.options.detail || ''}`;
|
||||
return typeLabel;
|
||||
}
|
||||
|
||||
updateMessage(message: string): void {
|
||||
@@ -218,6 +223,10 @@ export class Dialog extends Disposable {
|
||||
this._register(domEvent(window, 'keydown', true)((e: KeyboardEvent) => {
|
||||
const evt = new StandardKeyboardEvent(e);
|
||||
|
||||
if (evt.equals(KeyMod.Alt)) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
||||
if (evt.equals(KeyCode.Enter)) {
|
||||
|
||||
// Enter in input field should OK the dialog
|
||||
@@ -246,6 +255,17 @@ export class Dialog extends Disposable {
|
||||
// Build a list of focusable elements in their visual order
|
||||
const focusableElements: { focus: () => void }[] = [];
|
||||
let focusedIndex = -1;
|
||||
|
||||
if (this.messageContainer) {
|
||||
const links = this.messageContainer.querySelectorAll('a');
|
||||
for (const link of links) {
|
||||
focusableElements.push(link);
|
||||
if (link === document.activeElement) {
|
||||
focusedIndex = focusableElements.length - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const input of this.inputs) {
|
||||
focusableElements.push(input);
|
||||
if (input.hasFocus()) {
|
||||
@@ -371,7 +391,7 @@ export class Dialog extends Disposable {
|
||||
|
||||
this.applyStyles();
|
||||
|
||||
this.element.setAttribute('aria-label', this.getAriaLabel());
|
||||
this.element.setAttribute('aria-labelledby', 'monaco-dialog-icon monaco-dialog-message-text monaco-dialog-message-detail monaco-dialog-message-body');
|
||||
show(this.element);
|
||||
|
||||
// Focus first element (input or button)
|
||||
|
||||
@@ -37,3 +37,10 @@
|
||||
line-height: 16px;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.monaco-dropdown-with-primary > .dropdown-action-container > .monaco-dropdown > .dropdown-label > .action-label {
|
||||
display: block;
|
||||
background-size: 16px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export class BaseDropdown extends ActionRunner {
|
||||
for (const event of [EventType.MOUSE_DOWN, GestureEventType.Tap]) {
|
||||
this._register(addDisposableListener(this._label, event, e => {
|
||||
if (e instanceof MouseEvent && e.detail > 1) {
|
||||
return; // prevent multiple clicks to open multiple context menus (https://github.com/Microsoft/vscode/issues/41363)
|
||||
return; // prevent multiple clicks to open multiple context menus (https://github.com/microsoft/vscode/issues/41363)
|
||||
}
|
||||
|
||||
if (this.visible) {
|
||||
@@ -71,7 +71,7 @@ export class BaseDropdown extends ActionRunner {
|
||||
this._register(addDisposableListener(this._label, EventType.KEY_UP, e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
|
||||
EventHelper.stop(e, true); // https://github.com/Microsoft/vscode/issues/57997
|
||||
EventHelper.stop(e, true); // https://github.com/microsoft/vscode/issues/57997
|
||||
|
||||
if (this.visible) {
|
||||
this.hide();
|
||||
|
||||
@@ -62,4 +62,4 @@
|
||||
0% { background: rgba(255, 255, 255, 0.44); }
|
||||
/* Made intentionally different such that the CSS minifier does not collapse the two animations into a single one*/
|
||||
99% { background: transparent; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ export class FindInput extends Widget {
|
||||
const flexibleMaxHeight = options.flexibleMaxHeight;
|
||||
|
||||
this.domNode = document.createElement('div');
|
||||
dom.addClass(this.domNode, 'monaco-findInput');
|
||||
this.domNode.classList.add('monaco-findInput');
|
||||
|
||||
this.inputBox = this._register(new HistoryInputBox(this.domNode, this.contextViewProvider, {
|
||||
placeholder: this.placeholder || '',
|
||||
@@ -263,7 +263,7 @@ export class FindInput extends Widget {
|
||||
}
|
||||
|
||||
public enable(): void {
|
||||
dom.removeClass(this.domNode, 'disabled');
|
||||
this.domNode.classList.remove('disabled');
|
||||
this.inputBox.enable();
|
||||
this.regex.enable();
|
||||
this.wholeWords.enable();
|
||||
@@ -271,7 +271,7 @@ export class FindInput extends Widget {
|
||||
}
|
||||
|
||||
public disable(): void {
|
||||
dom.addClass(this.domNode, 'disabled');
|
||||
this.domNode.classList.add('disabled');
|
||||
this.inputBox.disable();
|
||||
this.regex.disable();
|
||||
this.wholeWords.disable();
|
||||
@@ -403,9 +403,9 @@ export class FindInput extends Widget {
|
||||
|
||||
private _lastHighlightFindOptions: number = 0;
|
||||
public highlightFindOptions(): void {
|
||||
dom.removeClass(this.domNode, 'highlight-' + (this._lastHighlightFindOptions));
|
||||
this.domNode.classList.remove('highlight-' + (this._lastHighlightFindOptions));
|
||||
this._lastHighlightFindOptions = 1 - this._lastHighlightFindOptions;
|
||||
dom.addClass(this.domNode, 'highlight-' + (this._lastHighlightFindOptions));
|
||||
this.domNode.classList.add('highlight-' + (this._lastHighlightFindOptions));
|
||||
}
|
||||
|
||||
public validate(): void {
|
||||
|
||||
@@ -136,7 +136,7 @@ export class ReplaceInput extends Widget {
|
||||
const flexibleMaxHeight = options.flexibleMaxHeight;
|
||||
|
||||
this.domNode = document.createElement('div');
|
||||
dom.addClass(this.domNode, 'monaco-findInput');
|
||||
this.domNode.classList.add('monaco-findInput');
|
||||
|
||||
this.inputBox = this._register(new HistoryInputBox(this.domNode, this.contextViewProvider, {
|
||||
ariaLabel: this.label || '',
|
||||
@@ -234,13 +234,13 @@ export class ReplaceInput extends Widget {
|
||||
}
|
||||
|
||||
public enable(): void {
|
||||
dom.removeClass(this.domNode, 'disabled');
|
||||
this.domNode.classList.remove('disabled');
|
||||
this.inputBox.enable();
|
||||
this.preserveCase.enable();
|
||||
}
|
||||
|
||||
public disable(): void {
|
||||
dom.addClass(this.domNode, 'disabled');
|
||||
this.domNode.classList.add('disabled');
|
||||
this.inputBox.disable();
|
||||
this.preserveCase.disable();
|
||||
}
|
||||
@@ -347,9 +347,9 @@ export class ReplaceInput extends Widget {
|
||||
|
||||
private _lastHighlightFindOptions: number = 0;
|
||||
public highlightFindOptions(): void {
|
||||
dom.removeClass(this.domNode, 'highlight-' + (this._lastHighlightFindOptions));
|
||||
this.domNode.classList.remove('highlight-' + (this._lastHighlightFindOptions));
|
||||
this._lastHighlightFindOptions = 1 - this._lastHighlightFindOptions;
|
||||
dom.addClass(this.domNode, 'highlight-' + (this._lastHighlightFindOptions));
|
||||
this.domNode.classList.add('highlight-' + (this._lastHighlightFindOptions));
|
||||
}
|
||||
|
||||
public validate(): void {
|
||||
|
||||
@@ -210,7 +210,9 @@ export class Grid<T extends IView = IView> extends Disposable {
|
||||
get minimumHeight(): number { return this.gridview.minimumHeight; }
|
||||
get maximumWidth(): number { return this.gridview.maximumWidth; }
|
||||
get maximumHeight(): number { return this.gridview.maximumHeight; }
|
||||
get onDidChange(): Event<{ width: number; height: number; } | undefined> { return this.gridview.onDidChange; }
|
||||
|
||||
readonly onDidChange: Event<{ width: number; height: number; } | undefined>;
|
||||
readonly onDidScroll: Event<void>;
|
||||
|
||||
get boundarySashes(): IBoundarySashes { return this.gridview.boundarySashes; }
|
||||
set boundarySashes(boundarySashes: IBoundarySashes) { this.gridview.boundarySashes = boundarySashes; }
|
||||
@@ -232,8 +234,8 @@ export class Grid<T extends IView = IView> extends Disposable {
|
||||
} else {
|
||||
this.gridview = new GridView(options);
|
||||
}
|
||||
this._register(this.gridview);
|
||||
|
||||
this._register(this.gridview);
|
||||
this._register(this.gridview.onDidSashReset(this.onDidSashReset, this));
|
||||
|
||||
const size: number | GridViewSizing = typeof options.firstViewVisibleCachedSize === 'number'
|
||||
@@ -243,6 +245,9 @@ export class Grid<T extends IView = IView> extends Disposable {
|
||||
if (!(view instanceof GridView)) {
|
||||
this._addView(view, size, [0]);
|
||||
}
|
||||
|
||||
this.onDidChange = this.gridview.onDidChange;
|
||||
this.onDidScroll = this.gridview.onDidScroll;
|
||||
}
|
||||
|
||||
style(styles: IGridStyles): void {
|
||||
|
||||
@@ -13,4 +13,4 @@
|
||||
.monaco-grid-branch-node {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ import { Orientation, Sash } from 'vs/base/browser/ui/sash/sash';
|
||||
import { SplitView, IView as ISplitView, Sizing, LayoutPriority, ISplitViewStyles } from 'vs/base/browser/ui/splitview/splitview';
|
||||
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { $ } from 'vs/base/browser/dom';
|
||||
import { tail2 as tail } from 'vs/base/common/arrays';
|
||||
import { equals as arrayEquals, tail2 as tail } from 'vs/base/common/arrays';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { clamp } from 'vs/base/common/numbers';
|
||||
import { isUndefined } from 'vs/base/common/types';
|
||||
|
||||
export { Sizing, LayoutPriority } from 'vs/base/browser/ui/splitview/splitview';
|
||||
export { Orientation } from 'vs/base/browser/ui/sash/sash';
|
||||
@@ -242,6 +243,10 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
|
||||
private readonly _onDidChange = new Emitter<number | undefined>();
|
||||
readonly onDidChange: Event<number | undefined> = this._onDidChange.event;
|
||||
|
||||
private _onDidScroll = new Emitter<void>();
|
||||
private onDidScrollDisposable: IDisposable = Disposable.None;
|
||||
readonly onDidScroll: Event<void> = this._onDidScroll.event;
|
||||
|
||||
private childrenChangeDisposable: IDisposable = Disposable.None;
|
||||
|
||||
private readonly _onDidSashReset = new Emitter<number[]>();
|
||||
@@ -343,11 +348,7 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
|
||||
const onDidSashReset = Event.map(this.splitview.onDidSashReset, i => [i]);
|
||||
this.splitviewSashResetDisposable = onDidSashReset(this._onDidSashReset.fire, this._onDidSashReset);
|
||||
|
||||
const onDidChildrenChange = Event.map(Event.any(...this.children.map(c => c.onDidChange)), () => undefined);
|
||||
this.childrenChangeDisposable = onDidChildrenChange(this._onDidChange.fire, this._onDidChange);
|
||||
|
||||
const onDidChildrenSashReset = Event.any(...this.children.map((c, i) => Event.map(c.onDidSashReset, location => [i, ...location])));
|
||||
this.childrenSashResetDisposable = onDidChildrenSashReset(this._onDidSashReset.fire, this._onDidSashReset);
|
||||
this.updateChildrenEvents();
|
||||
}
|
||||
|
||||
style(styles: IGridViewStyles): void {
|
||||
@@ -566,6 +567,11 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
|
||||
}
|
||||
|
||||
private onDidChildrenChange(): void {
|
||||
this.updateChildrenEvents();
|
||||
this._onDidChange.fire(undefined);
|
||||
}
|
||||
|
||||
private updateChildrenEvents(): void {
|
||||
const onDidChildrenChange = Event.map(Event.any(...this.children.map(c => c.onDidChange)), () => undefined);
|
||||
this.childrenChangeDisposable.dispose();
|
||||
this.childrenChangeDisposable = onDidChildrenChange(this._onDidChange.fire, this._onDidChange);
|
||||
@@ -574,7 +580,9 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
|
||||
this.childrenSashResetDisposable.dispose();
|
||||
this.childrenSashResetDisposable = onDidChildrenSashReset(this._onDidSashReset.fire, this._onDidSashReset);
|
||||
|
||||
this._onDidChange.fire(undefined);
|
||||
const onDidScroll = Event.any(Event.signal(this.splitview.onDidScroll), ...this.children.map(c => c.onDidScroll));
|
||||
this.onDidScrollDisposable.dispose();
|
||||
this.onDidScrollDisposable = onDidScroll(this._onDidScroll.fire, this._onDidScroll);
|
||||
}
|
||||
|
||||
trySet2x2(other: BranchNode): IDisposable {
|
||||
@@ -646,6 +654,25 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a latched event that avoids being fired when the view
|
||||
* constraints do not change at all.
|
||||
*/
|
||||
function createLatchedOnDidChangeViewEvent(view: IView): Event<IViewSize | undefined> {
|
||||
const [onDidChangeViewConstraints, onDidSetViewSize] = Event.split<undefined, IViewSize>(view.onDidChange, isUndefined);
|
||||
|
||||
return Event.any(
|
||||
onDidSetViewSize,
|
||||
Event.map(
|
||||
Event.latch(
|
||||
Event.map(onDidChangeViewConstraints, _ => ([view.minimumWidth, view.maximumWidth, view.minimumHeight, view.maximumHeight])),
|
||||
arrayEquals
|
||||
),
|
||||
_ => undefined
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
class LeafNode implements ISplitView<ILayoutContext>, IDisposable {
|
||||
|
||||
private _size: number = 0;
|
||||
@@ -657,6 +684,7 @@ class LeafNode implements ISplitView<ILayoutContext>, IDisposable {
|
||||
private absoluteOffset: number = 0;
|
||||
private absoluteOrthogonalOffset: number = 0;
|
||||
|
||||
readonly onDidScroll: Event<void> = Event.None;
|
||||
readonly onDidSashReset: Event<number[]> = Event.None;
|
||||
|
||||
private _onDidLinkedWidthNodeChange = new Relay<number | undefined>();
|
||||
@@ -691,7 +719,8 @@ class LeafNode implements ISplitView<ILayoutContext>, IDisposable {
|
||||
this._orthogonalSize = orthogonalSize;
|
||||
this._size = size;
|
||||
|
||||
this._onDidViewChange = Event.map(this.view.onDidChange, e => e && (this.orientation === Orientation.VERTICAL ? e.width : e.height));
|
||||
const onDidChange = createLatchedOnDidChangeViewEvent(view);
|
||||
this._onDidViewChange = Event.map(onDidChange, e => e && (this.orientation === Orientation.VERTICAL ? e.width : e.height));
|
||||
this.onDidChange = Event.any(this._onDidViewChange, this._onDidSetLinkedNode.event, this._onDidLinkedWidthNodeChange.event, this._onDidLinkedHeightNodeChange.event);
|
||||
}
|
||||
|
||||
@@ -778,7 +807,25 @@ class LeafNode implements ISplitView<ILayoutContext>, IDisposable {
|
||||
this._orthogonalSize = ctx.orthogonalSize;
|
||||
this.absoluteOffset = ctx.absoluteOffset + offset;
|
||||
this.absoluteOrthogonalOffset = ctx.absoluteOrthogonalOffset;
|
||||
this.view.layout(this.width, this.height, this.top, this.left);
|
||||
|
||||
this._layout(this.width, this.height, this.top, this.left);
|
||||
}
|
||||
|
||||
private cachedWidth: number = 0;
|
||||
private cachedHeight: number = 0;
|
||||
private cachedTop: number = 0;
|
||||
private cachedLeft: number = 0;
|
||||
|
||||
private _layout(width: number, height: number, top: number, left: number): void {
|
||||
if (this.cachedWidth === width && this.cachedHeight === height && this.cachedTop === top && this.cachedLeft === left) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cachedWidth = width;
|
||||
this.cachedHeight = height;
|
||||
this.cachedTop = top;
|
||||
this.cachedLeft = left;
|
||||
this.view.layout(width, height, top, left);
|
||||
}
|
||||
|
||||
setVisible(visible: boolean): void {
|
||||
@@ -852,6 +899,7 @@ export class GridView implements IDisposable {
|
||||
this.element.appendChild(root.element);
|
||||
this.onDidSashResetRelay.input = root.onDidSashReset;
|
||||
this._onDidChange.input = Event.map(root.onDidChange, () => undefined); // TODO
|
||||
this._onDidScroll.input = root.onDidScroll;
|
||||
}
|
||||
|
||||
get orientation(): Orientation {
|
||||
@@ -877,6 +925,9 @@ export class GridView implements IDisposable {
|
||||
get maximumWidth(): number { return this.root.maximumHeight; }
|
||||
get maximumHeight(): number { return this.root.maximumHeight; }
|
||||
|
||||
private _onDidScroll = new Relay<void>();
|
||||
readonly onDidScroll = this._onDidScroll.event;
|
||||
|
||||
private _onDidChange = new Relay<IViewSize | undefined>();
|
||||
readonly onDidChange = this._onDidChange.event;
|
||||
|
||||
|
||||
@@ -138,3 +138,8 @@
|
||||
margin-bottom: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.monaco-hover-content .action-container a {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@@ -16,9 +16,11 @@ export interface IHoverDelegateOptions {
|
||||
text: IMarkdownString | string;
|
||||
target: IHoverDelegateTarget | HTMLElement;
|
||||
hoverPosition?: HoverPosition;
|
||||
showPointer?: boolean;
|
||||
}
|
||||
|
||||
export interface IHoverDelegate {
|
||||
showHover(options: IHoverDelegateOptions): IDisposable | undefined;
|
||||
delay: number;
|
||||
placement?: 'mouse' | 'element';
|
||||
}
|
||||
|
||||
@@ -10,13 +10,10 @@ import { IMatch } from 'vs/base/common/filters';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Range } from 'vs/base/common/range';
|
||||
import { equals } from 'vs/base/common/objects';
|
||||
import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate';
|
||||
import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate';
|
||||
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';
|
||||
import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { setupCustomHover, setupNativeHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover';
|
||||
|
||||
export interface IIconLabelCreationOptions {
|
||||
supportHighlights?: boolean;
|
||||
@@ -91,17 +88,17 @@ class FastLabelNode {
|
||||
|
||||
export class IconLabel extends Disposable {
|
||||
|
||||
private domNode: FastLabelNode;
|
||||
private readonly domNode: FastLabelNode;
|
||||
|
||||
private nameNode: Label | LabelWithHighlights;
|
||||
private readonly nameNode: Label | LabelWithHighlights;
|
||||
|
||||
private descriptionContainer: FastLabelNode;
|
||||
private readonly descriptionContainer: FastLabelNode;
|
||||
private descriptionNode: FastLabelNode | HighlightedLabel | undefined;
|
||||
private descriptionNodeFactory: () => FastLabelNode | HighlightedLabel;
|
||||
private readonly descriptionNodeFactory: () => FastLabelNode | HighlightedLabel;
|
||||
|
||||
private labelContainer: HTMLElement;
|
||||
private readonly labelContainer: HTMLElement;
|
||||
|
||||
private hoverDelegate: IHoverDelegate | undefined = undefined;
|
||||
private readonly hoverDelegate: IHoverDelegate | undefined;
|
||||
private readonly customHovers: Map<HTMLElement, IDisposable> = new Map();
|
||||
|
||||
constructor(container: HTMLElement, options?: IIconLabelCreationOptions) {
|
||||
@@ -114,7 +111,7 @@ export class IconLabel extends Disposable {
|
||||
const nameContainer = dom.append(this.labelContainer, dom.$('span.monaco-icon-name-container'));
|
||||
this.descriptionContainer = this._register(new FastLabelNode(dom.append(this.labelContainer, dom.$('span.monaco-icon-description-container'))));
|
||||
|
||||
if (options?.supportHighlights) {
|
||||
if (options?.supportHighlights || options?.supportIcons) {
|
||||
this.nameNode = new LabelWithHighlights(nameContainer, !!options.supportIcons);
|
||||
} else {
|
||||
this.nameNode = new Label(nameContainer);
|
||||
@@ -126,9 +123,7 @@ export class IconLabel extends Disposable {
|
||||
this.descriptionNodeFactory = () => this._register(new FastLabelNode(dom.append(this.descriptionContainer.element, dom.$('span.label-description'))));
|
||||
}
|
||||
|
||||
if (options?.hoverDelegate) {
|
||||
this.hoverDelegate = options.hoverDelegate;
|
||||
}
|
||||
this.hoverDelegate = options?.hoverDelegate;
|
||||
}
|
||||
|
||||
get element(): HTMLElement {
|
||||
@@ -185,116 +180,21 @@ export class IconLabel extends Disposable {
|
||||
}
|
||||
|
||||
if (!this.hoverDelegate) {
|
||||
return this.setupNativeHover(htmlElement, tooltip);
|
||||
setupNativeHover(htmlElement, tooltip);
|
||||
} else {
|
||||
return this.setupCustomHover(this.hoverDelegate, htmlElement, tooltip);
|
||||
const hoverDisposable = setupCustomHover(this.hoverDelegate, htmlElement, tooltip);
|
||||
if (hoverDisposable) {
|
||||
this.customHovers.set(htmlElement, hoverDisposable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static adjustXAndShowCustomHover(hoverOptions: IHoverDelegateOptions | undefined, mouseX: number | undefined, hoverDelegate: IHoverDelegate, isHovering: boolean): IDisposable | undefined {
|
||||
if (hoverOptions && isHovering) {
|
||||
if (mouseX !== undefined) {
|
||||
(<IHoverDelegateTarget>hoverOptions.target).x = mouseX + 10;
|
||||
}
|
||||
return hoverDelegate.showHover(hoverOptions);
|
||||
public override dispose() {
|
||||
super.dispose();
|
||||
for (const disposable of this.customHovers.values()) {
|
||||
disposable.dispose();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getTooltipForCustom(markdownTooltip: string | IIconLabelMarkdownString): (token: CancellationToken) => Promise<string | IMarkdownString | undefined> {
|
||||
if (isString(markdownTooltip)) {
|
||||
return async () => markdownTooltip;
|
||||
} else if (isFunction(markdownTooltip.markdown)) {
|
||||
return markdownTooltip.markdown;
|
||||
} else {
|
||||
const markdown = markdownTooltip.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);
|
||||
|
||||
let hoverOptions: IHoverDelegateOptions | undefined;
|
||||
let mouseX: number | undefined;
|
||||
let isHovering = false;
|
||||
let tokenSource: CancellationTokenSource;
|
||||
let hoverDisposable: IDisposable | undefined;
|
||||
function mouseOver(this: HTMLElement, e: MouseEvent): void {
|
||||
if (isHovering) {
|
||||
return;
|
||||
}
|
||||
tokenSource = new CancellationTokenSource();
|
||||
function mouseLeaveOrDown(this: HTMLElement, e: MouseEvent): void {
|
||||
const isMouseDown = e.type === dom.EventType.MOUSE_DOWN;
|
||||
if (isMouseDown) {
|
||||
hoverDisposable?.dispose();
|
||||
hoverDisposable = undefined;
|
||||
}
|
||||
if (isMouseDown || (<any>e).fromElement === htmlElement) {
|
||||
isHovering = false;
|
||||
hoverOptions = undefined;
|
||||
tokenSource.dispose(true);
|
||||
mouseLeaveDisposable.dispose();
|
||||
mouseDownDisposable.dispose();
|
||||
}
|
||||
}
|
||||
const mouseLeaveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_LEAVE, true)(mouseLeaveOrDown.bind(htmlElement));
|
||||
const mouseDownDisposable = domEvent(htmlElement, dom.EventType.MOUSE_DOWN, true)(mouseLeaveOrDown.bind(htmlElement));
|
||||
isHovering = true;
|
||||
|
||||
function mouseMove(this: HTMLElement, e: MouseEvent): void {
|
||||
mouseX = e.x;
|
||||
}
|
||||
const mouseMoveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_MOVE, true)(mouseMove.bind(htmlElement));
|
||||
setTimeout(async () => {
|
||||
if (isHovering && tooltip) {
|
||||
// Re-use the already computed hover options if they exist.
|
||||
if (!hoverOptions) {
|
||||
const target: IHoverDelegateTarget = {
|
||||
targetElements: [this],
|
||||
dispose: () => { }
|
||||
};
|
||||
hoverOptions = {
|
||||
text: localize('iconLabel.loading', "Loading..."),
|
||||
target,
|
||||
hoverPosition: HoverPosition.BELOW
|
||||
};
|
||||
hoverDisposable = IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
|
||||
|
||||
const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined);
|
||||
if (resolvedTooltip) {
|
||||
hoverOptions = {
|
||||
text: resolvedTooltip,
|
||||
target,
|
||||
hoverPosition: HoverPosition.BELOW
|
||||
};
|
||||
// awaiting the tooltip could take a while. Make sure we're still hovering.
|
||||
hoverDisposable = IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
|
||||
} else if (hoverDisposable) {
|
||||
hoverDisposable.dispose();
|
||||
hoverDisposable = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
mouseMoveDisposable.dispose();
|
||||
}, hoverDelegate.delay);
|
||||
}
|
||||
const mouseOverDisposable = this._register(domEvent(htmlElement, dom.EventType.MOUSE_OVER, true)(mouseOver.bind(htmlElement)));
|
||||
this.customHovers.set(htmlElement, mouseOverDisposable);
|
||||
}
|
||||
|
||||
private setupNativeHover(htmlElement: HTMLElement, tooltip: string | IIconLabelMarkdownString | undefined): void {
|
||||
let stringTooltip: string = '';
|
||||
if (isString(tooltip)) {
|
||||
stringTooltip = tooltip;
|
||||
} else if (tooltip?.markdownNotSupportedFallback) {
|
||||
stringTooltip = tooltip.markdownNotSupportedFallback;
|
||||
}
|
||||
htmlElement.title = stringTooltip;
|
||||
this.customHovers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
130
src/vs/base/browser/ui/iconLabel/iconLabelHover.ts
Normal file
130
src/vs/base/browser/ui/iconLabel/iconLabelHover.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isFunction, isString } from 'vs/base/common/types';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IIconLabelMarkdownString } from 'vs/base/browser/ui/iconLabel/iconLabel';
|
||||
import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { DomEmitter } from 'vs/base/browser/event';
|
||||
import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
|
||||
|
||||
export function setupNativeHover(htmlElement: HTMLElement, tooltip: string | IIconLabelMarkdownString | undefined): void {
|
||||
if (isString(tooltip)) {
|
||||
htmlElement.title = tooltip;
|
||||
} else if (tooltip?.markdownNotSupportedFallback) {
|
||||
htmlElement.title = tooltip.markdownNotSupportedFallback;
|
||||
} else {
|
||||
htmlElement.removeAttribute('title');
|
||||
}
|
||||
}
|
||||
|
||||
export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString | undefined): IDisposable | undefined {
|
||||
if (!markdownTooltip) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const tooltip = getTooltipForCustom(markdownTooltip);
|
||||
|
||||
let hoverOptions: IHoverDelegateOptions | undefined;
|
||||
let mouseX: number | undefined;
|
||||
let isHovering = false;
|
||||
let tokenSource: CancellationTokenSource;
|
||||
let hoverDisposable: IDisposable | undefined;
|
||||
|
||||
const mouseOverDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_OVER, true);
|
||||
mouseOverDomEmitter.event((e: MouseEvent) => {
|
||||
if (isHovering) {
|
||||
return;
|
||||
}
|
||||
tokenSource = new CancellationTokenSource();
|
||||
function mouseLeaveOrDown(e: MouseEvent): void {
|
||||
const isMouseDown = e.type === dom.EventType.MOUSE_DOWN;
|
||||
if (isMouseDown) {
|
||||
hoverDisposable?.dispose();
|
||||
hoverDisposable = undefined;
|
||||
}
|
||||
if (isMouseDown || (<any>e).fromElement === htmlElement) {
|
||||
isHovering = false;
|
||||
hoverOptions = undefined;
|
||||
tokenSource.dispose(true);
|
||||
mouseLeaveDomEmitter.dispose();
|
||||
mouseDownDomEmitter.dispose();
|
||||
}
|
||||
}
|
||||
const mouseLeaveDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_LEAVE, true);
|
||||
mouseLeaveDomEmitter.event(mouseLeaveOrDown);
|
||||
const mouseDownDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_DOWN, true);
|
||||
mouseDownDomEmitter.event(mouseLeaveOrDown);
|
||||
isHovering = true;
|
||||
|
||||
function mouseMove(e: MouseEvent): void {
|
||||
mouseX = e.x;
|
||||
}
|
||||
const mouseMoveDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_MOVE, true);
|
||||
mouseMoveDomEmitter.event(mouseMove);
|
||||
setTimeout(async () => {
|
||||
if (isHovering && tooltip) {
|
||||
// Re-use the already computed hover options if they exist.
|
||||
if (!hoverOptions) {
|
||||
const target: IHoverDelegateTarget = {
|
||||
targetElements: [htmlElement],
|
||||
dispose: () => { }
|
||||
};
|
||||
hoverOptions = {
|
||||
text: localize('iconLabel.loading', "Loading..."),
|
||||
target,
|
||||
hoverPosition: HoverPosition.BELOW
|
||||
};
|
||||
hoverDisposable = adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
|
||||
|
||||
const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined);
|
||||
if (resolvedTooltip) {
|
||||
hoverOptions = {
|
||||
text: resolvedTooltip,
|
||||
target,
|
||||
showPointer: hoverDelegate.placement === 'element',
|
||||
hoverPosition: HoverPosition.BELOW
|
||||
};
|
||||
// awaiting the tooltip could take a while. Make sure we're still hovering.
|
||||
hoverDisposable = adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
|
||||
} else if (hoverDisposable) {
|
||||
hoverDisposable.dispose();
|
||||
hoverDisposable = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
mouseMoveDomEmitter.dispose();
|
||||
}, hoverDelegate.delay);
|
||||
});
|
||||
return mouseOverDomEmitter;
|
||||
}
|
||||
|
||||
|
||||
function getTooltipForCustom(markdownTooltip: string | IIconLabelMarkdownString): (token: CancellationToken) => Promise<string | IMarkdownString | undefined> {
|
||||
if (isString(markdownTooltip)) {
|
||||
return async () => markdownTooltip;
|
||||
} else if (isFunction(markdownTooltip.markdown)) {
|
||||
return markdownTooltip.markdown;
|
||||
} else {
|
||||
const markdown = markdownTooltip.markdown;
|
||||
return async () => markdown;
|
||||
}
|
||||
}
|
||||
|
||||
function adjustXAndShowCustomHover(hoverOptions: IHoverDelegateOptions | undefined, mouseX: number | undefined, hoverDelegate: IHoverDelegate, isHovering: boolean): IDisposable | undefined {
|
||||
if (hoverOptions && isHovering) {
|
||||
if (mouseX !== undefined && (hoverDelegate.placement === undefined || hoverDelegate.placement === 'mouse')) {
|
||||
(<IHoverDelegateTarget>hoverOptions.target).x = mouseX + 10;
|
||||
}
|
||||
return hoverDelegate.showHover(hoverOptions);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -28,7 +28,7 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
vertical-align: top;
|
||||
|
||||
flex-shrink: 0; /* fix for https://github.com/Microsoft/vscode/issues/13787 */
|
||||
flex-shrink: 0; /* fix for https://github.com/microsoft/vscode/issues/13787 */
|
||||
}
|
||||
|
||||
.monaco-icon-label > .monaco-icon-label-container {
|
||||
|
||||
@@ -898,7 +898,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
@memoize get onMouseOver(): Event<IListMouseEvent<T>> { return Event.map(domEvent(this.domNode, 'mouseover'), e => this.toMouseEvent(e)); }
|
||||
@memoize get onMouseMove(): Event<IListMouseEvent<T>> { return Event.map(domEvent(this.domNode, 'mousemove'), e => this.toMouseEvent(e)); }
|
||||
@memoize get onMouseOut(): Event<IListMouseEvent<T>> { return Event.map(domEvent(this.domNode, 'mouseout'), e => this.toMouseEvent(e)); }
|
||||
@memoize get onContextMenu(): Event<IListMouseEvent<T>> { return Event.map(domEvent(this.domNode, 'contextmenu'), e => this.toMouseEvent(e)); }
|
||||
@memoize get onContextMenu(): Event<IListMouseEvent<T> | IListGestureEvent<T>> { return Event.any(Event.map(domEvent(this.domNode, 'contextmenu'), e => this.toMouseEvent(e)), Event.map(domEvent(this.domNode, TouchEventType.Contextmenu) as Event<GestureEvent>, e => this.toGestureEvent(e))); }
|
||||
@memoize get onTouchStart(): Event<IListTouchEvent<T>> { return Event.map(domEvent(this.domNode, 'touchstart'), e => this.toTouchEvent(e)); }
|
||||
@memoize get onTap(): Event<IListGestureEvent<T>> { return Event.map(domEvent(this.rowsContainer, TouchEventType.Tap), e => this.toGestureEvent(e as GestureEvent)); }
|
||||
|
||||
|
||||
@@ -660,9 +660,15 @@ export class MouseController<T> implements IDisposable {
|
||||
|
||||
private changeSelection(e: IListMouseEvent<T> | IListTouchEvent<T>): void {
|
||||
const focus = e.index!;
|
||||
const anchor = this.list.getAnchor();
|
||||
let anchor = this.list.getAnchor();
|
||||
|
||||
if (this.isSelectionRangeChangeEvent(e)) {
|
||||
if (typeof anchor === 'undefined') {
|
||||
const currentFocus = this.list.getFocus()[0];
|
||||
anchor = currentFocus ?? focus;
|
||||
this.list.setAnchor(anchor);
|
||||
}
|
||||
|
||||
if (this.isSelectionRangeChangeEvent(e) && typeof anchor === 'number') {
|
||||
const min = Math.min(anchor, focus);
|
||||
const max = Math.max(anchor, focus);
|
||||
const rangeSelection = range(min, max + 1);
|
||||
@@ -1201,7 +1207,7 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
|
||||
|
||||
const fromMouse = Event.chain(this.view.onContextMenu)
|
||||
.filter(_ => !didJustPressContextMenuKey)
|
||||
.map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.clientX + 1, y: browserEvent.clientY }, browserEvent }))
|
||||
.map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.pageX + 1, y: browserEvent.pageY }, browserEvent }))
|
||||
.event;
|
||||
|
||||
return Event.any<IListContextMenuEvent<T>>(fromKeyDown, fromKeyUp, fromMouse);
|
||||
|
||||
@@ -16,4 +16,4 @@ export class CombinedSpliceable<T> implements ISpliceable<T> {
|
||||
splice(start: number, deleteCount: number, elements: T[]): void {
|
||||
this.spliceables.forEach(s => s.splice(start, deleteCount, elements));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,14 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./sash';
|
||||
import { IDisposable, dispose, Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { EventType, GestureEvent, Gesture } from 'vs/base/browser/touch';
|
||||
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { EventType, Gesture, GestureEvent } from 'vs/base/browser/touch';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { getElementsByTagName, EventHelper, createStyleSheet, addDisposableListener, append, $ } from 'vs/base/browser/dom';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { getElementsByTagName, EventHelper, createStyleSheet, append, $, EventLike } from 'vs/base/browser/dom';
|
||||
import { DomEmitter } from 'vs/base/browser/event';
|
||||
import { Delayer } from 'vs/base/common/async';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
|
||||
let DEBUG = false;
|
||||
// DEBUG = Boolean("true"); // done "weirdly" so that a lint warning prevents you from pushing this
|
||||
@@ -88,6 +87,78 @@ export function setGlobalHoverDelay(size: number): void {
|
||||
onDidChangeHoverDelay.fire(size);
|
||||
}
|
||||
|
||||
interface PointerEvent extends EventLike {
|
||||
readonly pageX: number;
|
||||
readonly pageY: number;
|
||||
readonly altKey: boolean;
|
||||
readonly target: EventTarget | null;
|
||||
}
|
||||
|
||||
interface IPointerEventFactory {
|
||||
readonly onPointerMove: Event<PointerEvent>;
|
||||
readonly onPointerUp: Event<PointerEvent>;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
class MouseEventFactory implements IPointerEventFactory {
|
||||
|
||||
private disposables = new DisposableStore();
|
||||
|
||||
@memoize
|
||||
get onPointerMove(): Event<PointerEvent> {
|
||||
return this.disposables.add(new DomEmitter(window, 'mousemove')).event;
|
||||
}
|
||||
|
||||
@memoize
|
||||
get onPointerUp(): Event<PointerEvent> {
|
||||
return this.disposables.add(new DomEmitter(window, 'mouseup')).event;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class GestureEventFactory implements IPointerEventFactory {
|
||||
|
||||
private disposables = new DisposableStore();
|
||||
|
||||
@memoize
|
||||
get onPointerMove(): Event<PointerEvent> {
|
||||
return this.disposables.add(new DomEmitter(this.el, EventType.Change)).event;
|
||||
}
|
||||
|
||||
@memoize
|
||||
get onPointerUp(): Event<PointerEvent> {
|
||||
return this.disposables.add(new DomEmitter(this.el, EventType.End)).event;
|
||||
}
|
||||
|
||||
constructor(private el: HTMLElement) { }
|
||||
|
||||
dispose(): void {
|
||||
this.disposables.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class OrthogonalPointerEventFactory implements IPointerEventFactory {
|
||||
|
||||
@memoize
|
||||
get onPointerMove(): Event<PointerEvent> {
|
||||
return this.factory.onPointerMove;
|
||||
}
|
||||
|
||||
@memoize
|
||||
get onPointerUp(): Event<PointerEvent> {
|
||||
return this.factory.onPointerUp;
|
||||
}
|
||||
|
||||
constructor(private factory: IPointerEventFactory) { }
|
||||
|
||||
dispose(): void {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
export class Sash extends Disposable {
|
||||
|
||||
private el: HTMLElement;
|
||||
@@ -146,9 +217,9 @@ export class Sash extends Disposable {
|
||||
if (state !== SashState.Disabled) {
|
||||
this._orthogonalStartDragHandle = append(this.el, $('.orthogonal-drag-handle.start'));
|
||||
this.orthogonalStartDragHandleDisposables.add(toDisposable(() => this._orthogonalStartDragHandle!.remove()));
|
||||
domEvent(this._orthogonalStartDragHandle, 'mouseenter')
|
||||
this.orthogonalEndDragHandleDisposables.add(new DomEmitter(this._orthogonalStartDragHandle, 'mouseenter')).event
|
||||
(() => Sash.onMouseEnter(sash), undefined, this.orthogonalStartDragHandleDisposables);
|
||||
domEvent(this._orthogonalStartDragHandle, 'mouseleave')
|
||||
this.orthogonalEndDragHandleDisposables.add(new DomEmitter(this._orthogonalStartDragHandle, 'mouseleave')).event
|
||||
(() => Sash.onMouseLeave(sash), undefined, this.orthogonalStartDragHandleDisposables);
|
||||
}
|
||||
};
|
||||
@@ -176,9 +247,9 @@ export class Sash extends Disposable {
|
||||
if (state !== SashState.Disabled) {
|
||||
this._orthogonalEndDragHandle = append(this.el, $('.orthogonal-drag-handle.end'));
|
||||
this.orthogonalEndDragHandleDisposables.add(toDisposable(() => this._orthogonalEndDragHandle!.remove()));
|
||||
domEvent(this._orthogonalEndDragHandle, 'mouseenter')
|
||||
this.orthogonalEndDragHandleDisposables.add(new DomEmitter(this._orthogonalEndDragHandle, 'mouseenter')).event
|
||||
(() => Sash.onMouseEnter(sash), undefined, this.orthogonalEndDragHandleDisposables);
|
||||
domEvent(this._orthogonalEndDragHandle, 'mouseleave')
|
||||
this.orthogonalEndDragHandleDisposables.add(new DomEmitter(this._orthogonalEndDragHandle, 'mouseleave')).event
|
||||
(() => Sash.onMouseLeave(sash), undefined, this.orthogonalEndDragHandleDisposables);
|
||||
}
|
||||
};
|
||||
@@ -205,13 +276,28 @@ export class Sash extends Disposable {
|
||||
this.el.classList.add('mac');
|
||||
}
|
||||
|
||||
this._register(domEvent(this.el, 'mousedown')(this.onMouseDown, this));
|
||||
this._register(domEvent(this.el, 'dblclick')(this.onMouseDoubleClick, this));
|
||||
this._register(domEvent(this.el, 'mouseenter')(() => Sash.onMouseEnter(this)));
|
||||
this._register(domEvent(this.el, 'mouseleave')(() => Sash.onMouseLeave(this)));
|
||||
const onMouseDown = this._register(new DomEmitter(this.el, 'mousedown')).event;
|
||||
this._register(onMouseDown(e => this.onPointerStart(e, new MouseEventFactory()), this));
|
||||
const onMouseDoubleClick = this._register(new DomEmitter(this.el, 'dblclick')).event;
|
||||
this._register(onMouseDoubleClick(this.onPointerDoublePress, this));
|
||||
const onMouseEnter = this._register(new DomEmitter(this.el, 'mouseenter')).event;
|
||||
this._register(onMouseEnter(() => Sash.onMouseEnter(this)));
|
||||
const onMouseLeave = this._register(new DomEmitter(this.el, 'mouseleave')).event;
|
||||
this._register(onMouseLeave(() => Sash.onMouseLeave(this)));
|
||||
|
||||
this._register(Gesture.addTarget(this.el));
|
||||
this._register(domEvent(this.el, EventType.Start)(e => this.onTouchStart(e as GestureEvent), this));
|
||||
|
||||
const onTouchStart = Event.map(this._register(new DomEmitter(this.el, EventType.Start)).event, e => ({ ...e, target: e.initialTarget ?? null }));
|
||||
this._register(onTouchStart(e => this.onPointerStart(e, new GestureEventFactory(this.el)), this));
|
||||
const onTap = this._register(new DomEmitter(this.el, EventType.Tap)).event;
|
||||
const onDoubleTap = Event.map(
|
||||
Event.filter(
|
||||
Event.debounce<GestureEvent, { event: GestureEvent, count: number }>(onTap, (res, event) => ({ event, count: (res?.count ?? 0) + 1 }), 250),
|
||||
({ count }) => count === 2
|
||||
),
|
||||
({ event }) => ({ ...event, target: event.initialTarget ?? null })
|
||||
);
|
||||
this._register(onDoubleTap(this.onPointerDoublePress, this));
|
||||
|
||||
if (typeof options.size === 'number') {
|
||||
this.size = options.size;
|
||||
@@ -252,24 +338,24 @@ export class Sash extends Disposable {
|
||||
this.layout();
|
||||
}
|
||||
|
||||
private onMouseDown(e: MouseEvent): void {
|
||||
EventHelper.stop(e, false);
|
||||
private onPointerStart(event: PointerEvent, pointerEventFactory: IPointerEventFactory): void {
|
||||
EventHelper.stop(event);
|
||||
|
||||
let isMultisashResize = false;
|
||||
|
||||
if (!(e as any).__orthogonalSashEvent) {
|
||||
const orthogonalSash = this.getOrthogonalSash(e);
|
||||
if (!(event as any).__orthogonalSashEvent) {
|
||||
const orthogonalSash = this.getOrthogonalSash(event);
|
||||
|
||||
if (orthogonalSash) {
|
||||
isMultisashResize = true;
|
||||
(e as any).__orthogonalSashEvent = true;
|
||||
orthogonalSash.onMouseDown(e);
|
||||
(event as any).__orthogonalSashEvent = true;
|
||||
orthogonalSash.onPointerStart(event, new OrthogonalPointerEventFactory(pointerEventFactory));
|
||||
}
|
||||
}
|
||||
|
||||
if (this.linkedSash && !(e as any).__linkedSashEvent) {
|
||||
(e as any).__linkedSashEvent = true;
|
||||
this.linkedSash.onMouseDown(e);
|
||||
if (this.linkedSash && !(event as any).__linkedSashEvent) {
|
||||
(event as any).__linkedSashEvent = true;
|
||||
this.linkedSash.onPointerStart(event, new OrthogonalPointerEventFactory(pointerEventFactory));
|
||||
}
|
||||
|
||||
if (!this.state) {
|
||||
@@ -287,10 +373,9 @@ export class Sash extends Disposable {
|
||||
iframe.style.pointerEvents = 'none'; // disable mouse events on iframes as long as we drag the sash
|
||||
}
|
||||
|
||||
const mouseDownEvent = new StandardMouseEvent(e);
|
||||
const startX = mouseDownEvent.posx;
|
||||
const startY = mouseDownEvent.posy;
|
||||
const altKey = mouseDownEvent.altKey;
|
||||
const startX = event.pageX;
|
||||
const startY = event.pageY;
|
||||
const altKey = event.altKey;
|
||||
const startEvent: ISashEvent = { startX, currentX: startX, startY, currentY: startY, altKey };
|
||||
|
||||
this.el.classList.add('active');
|
||||
@@ -332,15 +417,14 @@ export class Sash extends Disposable {
|
||||
this.onDidEnablementChange(updateStyle, null, disposables);
|
||||
}
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
EventHelper.stop(e, false);
|
||||
const mouseMoveEvent = new StandardMouseEvent(e);
|
||||
const event: ISashEvent = { startX, currentX: mouseMoveEvent.posx, startY, currentY: mouseMoveEvent.posy, altKey };
|
||||
const event: ISashEvent = { startX, currentX: e.pageX, startY, currentY: e.pageY, altKey };
|
||||
|
||||
this._onDidChange.fire(event);
|
||||
};
|
||||
|
||||
const onMouseUp = (e: MouseEvent) => {
|
||||
const onPointerUp = (e: PointerEvent) => {
|
||||
EventHelper.stop(e, false);
|
||||
|
||||
this.el.removeChild(style);
|
||||
@@ -355,11 +439,12 @@ export class Sash extends Disposable {
|
||||
}
|
||||
};
|
||||
|
||||
domEvent(window, 'mousemove')(onMouseMove, null, disposables);
|
||||
domEvent(window, 'mouseup')(onMouseUp, null, disposables);
|
||||
pointerEventFactory.onPointerMove(onPointerMove, null, disposables);
|
||||
pointerEventFactory.onPointerUp(onPointerUp, null, disposables);
|
||||
disposables.add(pointerEventFactory);
|
||||
}
|
||||
|
||||
private onMouseDoubleClick(e: MouseEvent): void {
|
||||
private onPointerDoublePress(e: MouseEvent): void {
|
||||
const orthogonalSash = this.getOrthogonalSash(e);
|
||||
|
||||
if (orthogonalSash) {
|
||||
@@ -373,41 +458,6 @@ export class Sash extends Disposable {
|
||||
this._onDidReset.fire();
|
||||
}
|
||||
|
||||
private onTouchStart(event: GestureEvent): void {
|
||||
EventHelper.stop(event);
|
||||
|
||||
const listeners: IDisposable[] = [];
|
||||
|
||||
const startX = event.pageX;
|
||||
const startY = event.pageY;
|
||||
const altKey = event.altKey;
|
||||
|
||||
this._onDidStart.fire({
|
||||
startX: startX,
|
||||
currentX: startX,
|
||||
startY: startY,
|
||||
currentY: startY,
|
||||
altKey
|
||||
});
|
||||
|
||||
listeners.push(addDisposableListener(this.el, EventType.Change, (event: GestureEvent) => {
|
||||
if (types.isNumber(event.pageX) && types.isNumber(event.pageY)) {
|
||||
this._onDidChange.fire({
|
||||
startX: startX,
|
||||
currentX: event.pageX,
|
||||
startY: startY,
|
||||
currentY: event.pageY,
|
||||
altKey
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
listeners.push(addDisposableListener(this.el, EventType.End, () => {
|
||||
this._onDidEnd.fire();
|
||||
dispose(listeners);
|
||||
}));
|
||||
}
|
||||
|
||||
private static onMouseEnter(sash: Sash, fromLinkedSash: boolean = false): void {
|
||||
if (sash.el.classList.contains('active')) {
|
||||
sash.hoverDelayer.cancel();
|
||||
@@ -476,7 +526,7 @@ export class Sash extends Disposable {
|
||||
return this.hidden;
|
||||
}
|
||||
|
||||
private getOrthogonalSash(e: MouseEvent): Sash | undefined {
|
||||
private getOrthogonalSash(e: PointerEvent): Sash | undefined {
|
||||
if (!e.target || !(e.target instanceof HTMLElement)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -330,7 +330,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
|
||||
}
|
||||
|
||||
if (this.styles.listFocusForeground) {
|
||||
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused:not(:hover) { color: ${this.styles.listFocusForeground} !important; }`);
|
||||
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused { color: ${this.styles.listFocusForeground} !important; }`);
|
||||
}
|
||||
|
||||
if (this.styles.decoratorRightForeground) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { isFirefox } from 'vs/base/browser/browser';
|
||||
import { DataTransfers } from 'vs/base/browser/dnd';
|
||||
import { Orientation } from 'vs/base/browser/ui/sash/sash';
|
||||
import { localize } from 'vs/nls';
|
||||
import { ScrollEvent } from 'vs/base/common/scrollable';
|
||||
|
||||
export interface IPaneOptions {
|
||||
minimumBodySize?: number;
|
||||
@@ -444,6 +445,7 @@ export class PaneView extends Disposable {
|
||||
|
||||
orientation: Orientation;
|
||||
readonly onDidSashChange: Event<number>;
|
||||
readonly onDidScroll: Event<ScrollEvent>;
|
||||
|
||||
constructor(container: HTMLElement, options: IPaneViewOptions = {}) {
|
||||
super();
|
||||
@@ -453,6 +455,7 @@ export class PaneView extends Disposable {
|
||||
this.element = append(container, $('.monaco-pane-view'));
|
||||
this.splitview = this._register(new SplitView(this.element, { orientation: this.orientation }));
|
||||
this.onDidSashChange = this.splitview.onDidSashChange;
|
||||
this.onDidScroll = this.splitview.onDidScroll;
|
||||
}
|
||||
|
||||
addPane(pane: Pane, size: number, index = this.splitview.length): void {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Color } from 'vs/base/common/color';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { $, append, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom';
|
||||
import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
import { Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import { Scrollable, ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable';
|
||||
export { Orientation } from 'vs/base/browser/ui/sash/sash';
|
||||
|
||||
export interface ISplitViewStyles {
|
||||
@@ -237,6 +237,8 @@ export class SplitView<TLayoutContext = undefined> extends Disposable {
|
||||
private _onDidSashReset = this._register(new Emitter<number>());
|
||||
readonly onDidSashReset = this._onDidSashReset.event;
|
||||
|
||||
readonly onDidScroll: Event<ScrollEvent>;
|
||||
|
||||
get length(): number {
|
||||
return this.viewItems.length;
|
||||
}
|
||||
@@ -317,7 +319,8 @@ export class SplitView<TLayoutContext = undefined> extends Disposable {
|
||||
horizontal: this.orientation === Orientation.HORIZONTAL ? (options.scrollbarVisibility ?? ScrollbarVisibility.Auto) : ScrollbarVisibility.Hidden
|
||||
}, this.scrollable));
|
||||
|
||||
this._register(this.scrollableElement.onScroll(e => {
|
||||
this.onDidScroll = this.scrollableElement.onScroll;
|
||||
this._register(this.onDidScroll(e => {
|
||||
this.viewContainer.scrollTop = e.scrollTop;
|
||||
this.viewContainer.scrollLeft = e.scrollLeft;
|
||||
}));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-table {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IListContextMenuEvent, IListEvent, IListGestureEvent, IListMouseEvent, IListRenderer, IListTouchEvent } from 'vs/base/browser/ui/list/list';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./table';
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
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 { splice, tail2 } from 'vs/base/common/arrays';
|
||||
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';
|
||||
@@ -256,7 +256,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
|
||||
}
|
||||
}
|
||||
|
||||
const deletedNodes = splice(parentNode.children, lastIndex, deleteCount, nodesToInsert);
|
||||
const deletedNodes = parentNode.children.splice(lastIndex, deleteCount, ...nodesToInsert);
|
||||
|
||||
// figure out what is the count of deleted visible children
|
||||
let deletedVisibleChildrenCount = 0;
|
||||
|
||||
@@ -31,14 +31,14 @@ export interface IAction extends IDisposable {
|
||||
enabled: boolean;
|
||||
checked: boolean;
|
||||
expanded: boolean | undefined; // {{SQL CARBON EDIT}}
|
||||
run(event?: unknown): Promise<unknown>;
|
||||
run(event?: unknown): Promise<unknown>; // {{SQL CARBON EDIT}} Add promise
|
||||
}
|
||||
|
||||
export interface IActionRunner extends IDisposable {
|
||||
readonly onDidRun: Event<IRunEvent>;
|
||||
readonly onBeforeRun: Event<IRunEvent>;
|
||||
|
||||
run(action: IAction, context?: unknown): Promise<unknown>;
|
||||
run(action: IAction, context?: unknown): Promise<unknown>; // {{SQL CARBON EDIT}} Add promise
|
||||
}
|
||||
|
||||
export interface IActionChangeEvent {
|
||||
@@ -62,9 +62,9 @@ export class Action extends Disposable implements IAction {
|
||||
protected _enabled: boolean = true;
|
||||
protected _checked: boolean = false;
|
||||
protected _expanded: boolean = false; // {{SQL CARBON EDIT}}
|
||||
protected readonly _actionCallback?: (event?: unknown) => Promise<unknown>;
|
||||
protected readonly _actionCallback?: (event?: unknown) => Promise<unknown>; // {{SQL CARBON EDIT}} Add promise
|
||||
|
||||
constructor(id: string, label: string = '', cssClass: string = '', enabled: boolean = true, actionCallback?: (event?: unknown) => Promise<unknown>) {
|
||||
constructor(id: string, label: string = '', cssClass: string = '', enabled: boolean = true, actionCallback?: (event?: unknown) => Promise<unknown>) { // {{SQL CARBON EDIT}} Add promise
|
||||
super();
|
||||
this._id = id;
|
||||
this._label = label;
|
||||
@@ -212,6 +212,24 @@ export class ActionRunner extends Disposable implements IActionRunner {
|
||||
|
||||
export class Separator extends Action {
|
||||
|
||||
/**
|
||||
* Joins all non-empty lists of actions with separators.
|
||||
*/
|
||||
public static join(...actionLists: readonly IAction[][]) {
|
||||
let out: IAction[] = [];
|
||||
for (const list of actionLists) {
|
||||
if (!list.length) {
|
||||
// skip
|
||||
} else if (out.length) {
|
||||
out = [...out, new Separator(), ...list];
|
||||
} else {
|
||||
out = list;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static readonly ID = 'vs.actions.separator';
|
||||
|
||||
constructor(label?: string) {
|
||||
@@ -257,6 +275,7 @@ export class SubmenuAction implements IAction {
|
||||
}
|
||||
protected _setExpanded(value: boolean): void {
|
||||
}
|
||||
// {{SQL CARBON EDIT}} - End
|
||||
}
|
||||
|
||||
export class EmptySubmenuAction extends Action {
|
||||
|
||||
@@ -385,16 +385,6 @@ export function lastIndex<T>(array: ReadonlyArray<T>, fn: (item: T) => boolean):
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated ES6: use `Array.find`
|
||||
*/
|
||||
export function first<T>(array: ReadonlyArray<T>, fn: (item: T) => boolean, notFoundValue: T): T;
|
||||
export function first<T>(array: ReadonlyArray<T>, fn: (item: T) => boolean): T | undefined;
|
||||
export function first<T>(array: ReadonlyArray<T>, fn: (item: T) => boolean, notFoundValue: T | undefined = undefined): T | undefined {
|
||||
const index = array.findIndex(fn);
|
||||
return index < 0 ? notFoundValue : array[index];
|
||||
}
|
||||
|
||||
export function firstOrDefault<T, NotFound = T>(array: ReadonlyArray<T>, notFoundValue: NotFound): T | NotFound;
|
||||
export function firstOrDefault<T>(array: ReadonlyArray<T>): T | undefined;
|
||||
export function firstOrDefault<T, NotFound = T>(array: ReadonlyArray<T>, notFoundValue?: NotFound): T | NotFound | undefined {
|
||||
@@ -566,76 +556,35 @@ export function mapFind<T, R>(array: Iterable<T>, mapFn: (value: T) => R | undef
|
||||
}
|
||||
|
||||
/**
|
||||
* An alternative for Array.push(...items) method, Use this if you need to push large number of items to the array.
|
||||
* Array.push(...item) can only support limited number of items due to the maximum call stack size limit.
|
||||
* @param array The array to add items to.
|
||||
* @param items The new items to be added.
|
||||
* Like Math.min with a delegate, and returns the winning index
|
||||
*/
|
||||
export function push<T>(array: T[], items: T[]): void {
|
||||
// set array length and then assign value at index is faster than doing array.push(item) individually
|
||||
const newLength = array.length + items.length;
|
||||
const startIdx = array.length;
|
||||
array.length = newLength;
|
||||
items.forEach((item, index) => {
|
||||
array[startIdx + index] = item;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the new items to array, the insertion be performed on the original array directly, the alternative insertArray() method will return a new array.
|
||||
* @param array The original array.
|
||||
* @param start The zero-based location in the array from which to start inserting elements.
|
||||
* @param newItems The items to be inserted
|
||||
*/
|
||||
export function insertArray2<T>(array: T[], start: number, newItems: T[]): void {
|
||||
const startIdx = getActualStartIndex(array, start);
|
||||
const originalLength = array.length;
|
||||
const newItemsLength = newItems.length;
|
||||
array.length = originalLength + newItemsLength;
|
||||
// Move the items after the start index, start from the end so that we don't overwrite any value.
|
||||
for (let i = originalLength - 1; i >= startIdx; i--) {
|
||||
array[i + newItemsLength] = array[i];
|
||||
}
|
||||
|
||||
for (let i = 0; i < newItemsLength; i++) {
|
||||
array[i + startIdx] = newItems[i];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes elements from an array and inserts new elements in their place, returning the deleted elements. Alternative to the native Array.splice method, it
|
||||
* can only support limited number of items due to the maximum call stack size limit.
|
||||
* @param array The original array.
|
||||
* @param start The zero-based location in the array from which to start removing elements.
|
||||
* @param deleteCount The number of elements to remove.
|
||||
* @returns An array containing the elements that were deleted.
|
||||
*/
|
||||
export function splice<T>(array: T[], start: number, deleteCount: number, newItems: T[]): T[] {
|
||||
const startIdx = getActualStartIndex(array, start);
|
||||
const deletedItems = array.splice(startIdx, deleteCount);
|
||||
insertArray2(array, startIdx, newItems);
|
||||
return deletedItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the actual start index (same logic as the native splice() or slice())
|
||||
* If greater than the length of the array, start will be set to the length of the array. In this case, no element will be deleted but the method will behave as an adding function, adding as many element as item[n*] provided.
|
||||
* If negative, it will begin that many elements from the end of the array. (In this case, the origin -1, meaning -n is the index of the nth last element, and is therefore equivalent to the index of array.length - n.) If array.length + start is less than 0, it will begin from index 0.
|
||||
* @param array The target array.
|
||||
* @param start The operation index.
|
||||
*/
|
||||
function getActualStartIndex<T>(array: T[], start: number): number {
|
||||
let startIndex: number;
|
||||
if (start > array.length) {
|
||||
startIndex = array.length;
|
||||
} else if (start < 0) {
|
||||
if ((start + array.length) < 0) {
|
||||
startIndex = 0;
|
||||
} else {
|
||||
startIndex = start + array.length;
|
||||
export function minIndex<T>(array: readonly T[], fn: (value: T) => number): number {
|
||||
let minValue = Number.MAX_SAFE_INTEGER;
|
||||
let minIdx = 0;
|
||||
array.forEach((value, i) => {
|
||||
const thisValue = fn(value);
|
||||
if (thisValue < minValue) {
|
||||
minValue = thisValue;
|
||||
minIdx = i;
|
||||
}
|
||||
} else {
|
||||
startIndex = start;
|
||||
}
|
||||
return startIndex;
|
||||
});
|
||||
|
||||
return minIdx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like Math.max with a delegate, and returns the winning index
|
||||
*/
|
||||
export function maxIndex<T>(array: readonly T[], fn: (value: T) => number): number {
|
||||
let minValue = Number.MIN_SAFE_INTEGER;
|
||||
let maxIdx = 0;
|
||||
array.forEach((value, i) => {
|
||||
const thisValue = fn(value);
|
||||
if (thisValue > minValue) {
|
||||
minValue = thisValue;
|
||||
maxIdx = i;
|
||||
}
|
||||
});
|
||||
|
||||
return maxIdx;
|
||||
}
|
||||
|
||||
@@ -536,7 +536,6 @@ export class Limiter<T> {
|
||||
|
||||
get size(): number {
|
||||
return this._size;
|
||||
// return this.runningPromises + this.outstandingPromises.length;
|
||||
}
|
||||
|
||||
queue(factory: ITask<Promise<T>>): Promise<T> {
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SqlIconId } from 'sql/base/common/codicons';
|
||||
import { codiconStartMarker } from 'vs/base/common/codicon';
|
||||
import { SqlIconId } from 'sql/base/common/codicons'; // {{SQL CARBON EDIT}}
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
export interface IIconRegistry {
|
||||
readonly all: IterableIterator<Codicon>;
|
||||
@@ -48,8 +46,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);
|
||||
}
|
||||
|
||||
// Selects all codicon names encapsulated in the `$()` syntax and wraps the
|
||||
@@ -114,7 +112,6 @@ export namespace CSSIcon {
|
||||
}
|
||||
return classNames;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function asClassName(icon: CSSIcon): string {
|
||||
@@ -586,36 +583,8 @@ export namespace Codicon {
|
||||
export const filterFilled = new Codicon('filter-filled', { fontCharacter: '\\ebce' });
|
||||
export const wand = new Codicon('wand', { fontCharacter: '\\ebcf' });
|
||||
export const debugLineByLine = new Codicon('debug-line-by-line', { fontCharacter: '\\ebd0' });
|
||||
export const inspect = new Codicon('inspect', { fontCharacter: '\\ebd1' });
|
||||
|
||||
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 || '');
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ export function forEach<T>(from: IStringDictionary<T> | INumberDictionary<T>, ca
|
||||
* Groups the collection into a dictionary based on the provided
|
||||
* group function.
|
||||
*/
|
||||
export function groupBy<T>(data: T[], groupFn: (element: T) => string): IStringDictionary<T[]> {
|
||||
const result: IStringDictionary<T[]> = Object.create(null);
|
||||
export function groupBy<K extends string | number | symbol, V>(data: V[], groupFn: (element: V) => K): Record<K, V[]> {
|
||||
const result: Record<K, V[]> = Object.create(null);
|
||||
for (const element of data) {
|
||||
const key = groupFn(element);
|
||||
let target = result[key];
|
||||
@@ -68,24 +68,6 @@ export function groupBy<T>(data: T[], groupFn: (element: T) => string): IStringD
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups the collection into a dictionary based on the provided
|
||||
* group function.
|
||||
*/
|
||||
export function groupByNumber<T>(data: T[], groupFn: (element: T) => number): Map<number, T[]> {
|
||||
const result = new Map<number, T[]>();
|
||||
for (const element of data) {
|
||||
const key = groupFn(element);
|
||||
let target = result.get(key);
|
||||
if (!target) {
|
||||
target = [];
|
||||
result.set(key, target);
|
||||
}
|
||||
target.push(element);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function fromMap<T>(original: Map<string, T>): IStringDictionary<T> {
|
||||
const result: IStringDictionary<T> = Object.create(null);
|
||||
if (original) {
|
||||
|
||||
@@ -523,7 +523,7 @@ export namespace Color {
|
||||
/**
|
||||
* The default format will use HEX if opaque and RGBA otherwise.
|
||||
*/
|
||||
export function format(color: Color): string | null {
|
||||
export function format(color: Color): string {
|
||||
if (color.isOpaque()) {
|
||||
return Color.Format.CSS.formatHex(color);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
import { sep } from 'vs/base/common/path';
|
||||
import { IdleValue } from 'vs/base/common/async';
|
||||
|
||||
// When comparing large numbers of strings, such as in sorting large arrays, is better for
|
||||
// performance to create an Intl.Collator object and use the function provided by its compare
|
||||
// property than it is to use String.prototype.localeCompare()
|
||||
// When comparing large numbers of strings it's better for performance to create an
|
||||
// Intl.Collator object and use the function provided by its compare property
|
||||
// than it is to use String.prototype.localeCompare()
|
||||
|
||||
// A collator with numeric sorting enabled, and no sensitivity to case or to accents
|
||||
// A collator with numeric sorting enabled, and no sensitivity to case, accents or diacritics.
|
||||
const intlFileNameCollatorBaseNumeric: IdleValue<{ collator: Intl.Collator, collatorIsNumeric: boolean }> = new IdleValue(() => {
|
||||
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
|
||||
return {
|
||||
@@ -28,19 +28,20 @@ const intlFileNameCollatorNumeric: IdleValue<{ collator: Intl.Collator }> = new
|
||||
});
|
||||
|
||||
// A collator with numeric sorting enabled, and sensitivity to accents and diacritics but not case.
|
||||
const intlFileNameCollatorNumericCaseInsenstive: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => {
|
||||
const intlFileNameCollatorNumericCaseInsensitive: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => {
|
||||
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'accent' });
|
||||
return {
|
||||
collator: collator
|
||||
};
|
||||
});/** Compares filenames without distinguishing the name from the extension. Disambiguates by unicode comparison. */
|
||||
});
|
||||
|
||||
/** Compares filenames without distinguishing the name from the extension. Disambiguates by unicode comparison. */
|
||||
export function compareFileNames(one: string | null, other: string | null, caseSensitive = false): number {
|
||||
const a = one || '';
|
||||
const b = other || '';
|
||||
const result = intlFileNameCollatorBaseNumeric.value.collator.compare(a, b);
|
||||
|
||||
// Using the numeric option in the collator will
|
||||
// make compare(`foo1`, `foo01`) === 0. We must disambiguate.
|
||||
// Using the numeric option will make compare(`foo1`, `foo01`) === 0. Disambiguate.
|
||||
if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && result === 0 && a !== b) {
|
||||
return a < b ? -1 : 1;
|
||||
}
|
||||
@@ -48,16 +49,45 @@ export function compareFileNames(one: string | null, other: string | null, caseS
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Compares filenames without distinguishing the name from the extension. Disambiguates by length, not unicode comparison. */
|
||||
/** Compares full filenames without grouping by case. */
|
||||
export function compareFileNamesDefault(one: string | null, other: string | null): number {
|
||||
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
|
||||
one = one || '';
|
||||
other = other || '';
|
||||
|
||||
// Compare the entire filename - both name and extension - and disambiguate by length if needed
|
||||
return compareAndDisambiguateByLength(collatorNumeric, one, other);
|
||||
}
|
||||
|
||||
/** Compares full filenames grouping uppercase names before lowercase. */
|
||||
export function compareFileNamesUpper(one: string | null, other: string | null) {
|
||||
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
|
||||
one = one || '';
|
||||
other = other || '';
|
||||
|
||||
return compareCaseUpperFirst(one, other) || compareAndDisambiguateByLength(collatorNumeric, one, other);
|
||||
}
|
||||
|
||||
/** Compares full filenames grouping lowercase names before uppercase. */
|
||||
export function compareFileNamesLower(one: string | null, other: string | null) {
|
||||
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
|
||||
one = one || '';
|
||||
other = other || '';
|
||||
|
||||
return compareCaseLowerFirst(one, other) || compareAndDisambiguateByLength(collatorNumeric, one, other);
|
||||
}
|
||||
|
||||
/** Compares full filenames by unicode value. */
|
||||
export function compareFileNamesUnicode(one: string | null, other: string | null) {
|
||||
one = one || '';
|
||||
other = other || '';
|
||||
|
||||
if (one === other) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return one < other ? -1 : 1;
|
||||
}
|
||||
|
||||
export function noIntlCompareFileNames(one: string | null, other: string | null, caseSensitive = false): number {
|
||||
if (!caseSensitive) {
|
||||
one = one && one.toLowerCase();
|
||||
@@ -78,6 +108,7 @@ export function noIntlCompareFileNames(one: string | null, other: string | null,
|
||||
return oneExtension < otherExtension ? -1 : 1;
|
||||
}
|
||||
|
||||
/** Compares filenames by extension, then by name. Disambiguates by unicode comparison. */
|
||||
export function compareFileExtensions(one: string | null, other: string | null): number {
|
||||
const [oneName, oneExtension] = extractNameAndExtension(one);
|
||||
const [otherName, otherExtension] = extractNameAndExtension(other);
|
||||
@@ -85,8 +116,7 @@ export function compareFileExtensions(one: string | null, other: string | null):
|
||||
let result = intlFileNameCollatorBaseNumeric.value.collator.compare(oneExtension, otherExtension);
|
||||
|
||||
if (result === 0) {
|
||||
// Using the numeric option in the collator will
|
||||
// make compare(`foo1`, `foo01`) === 0. We must disambiguate.
|
||||
// Using the numeric option will make compare(`foo1`, `foo01`) === 0. Disambiguate.
|
||||
if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && oneExtension !== otherExtension) {
|
||||
return oneExtension < otherExtension ? -1 : 1;
|
||||
}
|
||||
@@ -102,24 +132,65 @@ export function compareFileExtensions(one: string | null, other: string | null):
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Compares filenames by extenson, then by full filename */
|
||||
/** Compares filenames by extenson, then by full filename. Mixes uppercase and lowercase names together. */
|
||||
export function compareFileExtensionsDefault(one: string | null, other: string | null): number {
|
||||
one = one || '';
|
||||
other = other || '';
|
||||
const oneExtension = extractExtension(one);
|
||||
const otherExtension = extractExtension(other);
|
||||
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
|
||||
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator;
|
||||
let result;
|
||||
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsensitive.value.collator;
|
||||
|
||||
// Check for extension differences, ignoring differences in case and comparing numbers numerically.
|
||||
result = compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension);
|
||||
if (result !== 0) {
|
||||
return result;
|
||||
return compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension) ||
|
||||
compareAndDisambiguateByLength(collatorNumeric, one, other);
|
||||
}
|
||||
|
||||
/** Compares filenames by extension, then case, then full filename. Groups uppercase names before lowercase. */
|
||||
export function compareFileExtensionsUpper(one: string | null, other: string | null): number {
|
||||
one = one || '';
|
||||
other = other || '';
|
||||
const oneExtension = extractExtension(one);
|
||||
const otherExtension = extractExtension(other);
|
||||
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
|
||||
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsensitive.value.collator;
|
||||
|
||||
return compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension) ||
|
||||
compareCaseUpperFirst(one, other) ||
|
||||
compareAndDisambiguateByLength(collatorNumeric, one, other);
|
||||
}
|
||||
|
||||
/** Compares filenames by extension, then case, then full filename. Groups lowercase names before uppercase. */
|
||||
export function compareFileExtensionsLower(one: string | null, other: string | null): number {
|
||||
one = one || '';
|
||||
other = other || '';
|
||||
const oneExtension = extractExtension(one);
|
||||
const otherExtension = extractExtension(other);
|
||||
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
|
||||
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsensitive.value.collator;
|
||||
|
||||
return compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension) ||
|
||||
compareCaseLowerFirst(one, other) ||
|
||||
compareAndDisambiguateByLength(collatorNumeric, one, other);
|
||||
}
|
||||
|
||||
/** Compares filenames by case-insensitive extension unicode value, then by full filename unicode value. */
|
||||
export function compareFileExtensionsUnicode(one: string | null, other: string | null) {
|
||||
one = one || '';
|
||||
other = other || '';
|
||||
const oneExtension = extractExtension(one).toLowerCase();
|
||||
const otherExtension = extractExtension(other).toLowerCase();
|
||||
|
||||
// Check for extension differences
|
||||
if (oneExtension !== otherExtension) {
|
||||
return oneExtension < otherExtension ? -1 : 1;
|
||||
}
|
||||
|
||||
// Compare full filenames
|
||||
return compareAndDisambiguateByLength(collatorNumeric, one, other);
|
||||
// Check for full filename differences.
|
||||
if (one !== other) {
|
||||
return one < other ? -1 : 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const FileNameMatch = /^(.*?)(\.([^.]*))?$/;
|
||||
@@ -130,7 +201,7 @@ function extractNameAndExtension(str?: string | null, dotfilesAsNames = false):
|
||||
|
||||
let result: [string, string] = [(match && match[1]) || '', (match && match[3]) || ''];
|
||||
|
||||
// if the dotfilesAsNames option is selected, treat an empty filename with an extension,
|
||||
// if the dotfilesAsNames option is selected, treat an empty filename with an extension
|
||||
// or a filename that starts with a dot, as a dotfile name
|
||||
if (dotfilesAsNames && (!result[0] && result[1] || result[0] && result[0].charAt(0) === '.')) {
|
||||
result = [result[0] + '.' + result[1], ''];
|
||||
@@ -162,6 +233,54 @@ function compareAndDisambiguateByLength(collator: Intl.Collator, one: string, ot
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** @returns `true` if the string is starts with a lowercase letter. Otherwise, `false`. */
|
||||
function startsWithLower(string: string) {
|
||||
const character = string.charAt(0);
|
||||
|
||||
return (character.toLocaleUpperCase() !== character) ? true : false;
|
||||
}
|
||||
|
||||
/** @returns `true` if the string starts with an uppercase letter. Otherwise, `false`. */
|
||||
function startsWithUpper(string: string) {
|
||||
const character = string.charAt(0);
|
||||
|
||||
return (character.toLocaleLowerCase() !== character) ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the case of the provided strings - lowercase before uppercase
|
||||
*
|
||||
* @returns
|
||||
* ```text
|
||||
* -1 if one is lowercase and other is uppercase
|
||||
* 1 if one is uppercase and other is lowercase
|
||||
* 0 otherwise
|
||||
* ```
|
||||
*/
|
||||
function compareCaseLowerFirst(one: string, other: string): number {
|
||||
if (startsWithLower(one) && startsWithUpper(other)) {
|
||||
return -1;
|
||||
}
|
||||
return (startsWithUpper(one) && startsWithLower(other)) ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the case of the provided strings - uppercase before lowercase
|
||||
*
|
||||
* @returns
|
||||
* ```text
|
||||
* -1 if one is uppercase and other is lowercase
|
||||
* 1 if one is lowercase and other is uppercase
|
||||
* 0 otherwise
|
||||
* ```
|
||||
*/
|
||||
function compareCaseUpperFirst(one: string, other: string): number {
|
||||
if (startsWithUpper(one) && startsWithLower(other)) {
|
||||
return -1;
|
||||
}
|
||||
return (startsWithLower(one) && startsWithUpper(other)) ? 1 : 0;
|
||||
}
|
||||
|
||||
function comparePathComponents(one: string, other: string, caseSensitive = false): number {
|
||||
if (!caseSensitive) {
|
||||
one = one && one.toLowerCase();
|
||||
|
||||
@@ -24,64 +24,39 @@ export function createDecorator(mapFn: (fn: Function, key: string) => Function):
|
||||
};
|
||||
}
|
||||
|
||||
let memoizeId = 0;
|
||||
export function createMemoizer() {
|
||||
const memoizeKeyPrefix = `$memoize${memoizeId++}`;
|
||||
let self: any = undefined;
|
||||
export function memoize(_target: any, key: string, descriptor: any) {
|
||||
let fnKey: string | null = null;
|
||||
let fn: Function | null = null;
|
||||
|
||||
const result = function memoize(target: any, key: string, descriptor: any) {
|
||||
let fnKey: string | null = null;
|
||||
let fn: Function | null = null;
|
||||
if (typeof descriptor.value === 'function') {
|
||||
fnKey = 'value';
|
||||
fn = descriptor.value;
|
||||
|
||||
if (typeof descriptor.value === 'function') {
|
||||
fnKey = 'value';
|
||||
fn = descriptor.value;
|
||||
if (fn!.length !== 0) {
|
||||
console.warn('Memoize should only be used in functions with zero parameters');
|
||||
}
|
||||
} else if (typeof descriptor.get === 'function') {
|
||||
fnKey = 'get';
|
||||
fn = descriptor.get;
|
||||
}
|
||||
|
||||
if (fn!.length !== 0) {
|
||||
console.warn('Memoize should only be used in functions with zero parameters');
|
||||
}
|
||||
} else if (typeof descriptor.get === 'function') {
|
||||
fnKey = 'get';
|
||||
fn = descriptor.get;
|
||||
if (!fn) {
|
||||
throw new Error('not supported');
|
||||
}
|
||||
|
||||
const memoizeKey = `$memoize$${key}`;
|
||||
descriptor[fnKey!] = function (...args: any[]) {
|
||||
if (!this.hasOwnProperty(memoizeKey)) {
|
||||
Object.defineProperty(this, memoizeKey, {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: fn!.apply(this, args)
|
||||
});
|
||||
}
|
||||
|
||||
if (!fn) {
|
||||
throw new Error('not supported');
|
||||
}
|
||||
|
||||
const memoizeKey = `${memoizeKeyPrefix}:${key}`;
|
||||
descriptor[fnKey!] = function (...args: any[]) {
|
||||
self = this;
|
||||
|
||||
if (!this.hasOwnProperty(memoizeKey)) {
|
||||
Object.defineProperty(this, memoizeKey, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: fn!.apply(this, args)
|
||||
});
|
||||
}
|
||||
|
||||
return this[memoizeKey];
|
||||
};
|
||||
return this[memoizeKey];
|
||||
};
|
||||
|
||||
result.clear = () => {
|
||||
if (typeof self === 'undefined') {
|
||||
return;
|
||||
}
|
||||
Object.getOwnPropertyNames(self).forEach(property => {
|
||||
if (property.indexOf(memoizeKeyPrefix) === 0) {
|
||||
delete self[property];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function memoize(target: any, key: string, descriptor: any) {
|
||||
return createMemoizer()(target, key, descriptor);
|
||||
}
|
||||
|
||||
export interface IDebounceReducer<T> {
|
||||
|
||||
@@ -27,6 +27,7 @@ export function stringDiff(original: string, modified: string, pretty: boolean):
|
||||
|
||||
export interface ISequence {
|
||||
getElements(): Int32Array | number[] | string[];
|
||||
getStrictElement?(index: number): string;
|
||||
}
|
||||
|
||||
export interface IDiffChange {
|
||||
@@ -231,6 +232,8 @@ export class LcsDiff {
|
||||
|
||||
private readonly ContinueProcessingPredicate: IContinueProcessingPredicate | null;
|
||||
|
||||
private readonly _originalSequence: ISequence;
|
||||
private readonly _modifiedSequence: ISequence;
|
||||
private readonly _hasStrings: boolean;
|
||||
private readonly _originalStringElements: string[];
|
||||
private readonly _originalElementsOrHash: Int32Array;
|
||||
@@ -246,6 +249,9 @@ export class LcsDiff {
|
||||
constructor(originalSequence: ISequence, modifiedSequence: ISequence, continueProcessingPredicate: IContinueProcessingPredicate | null = null) {
|
||||
this.ContinueProcessingPredicate = continueProcessingPredicate;
|
||||
|
||||
this._originalSequence = originalSequence;
|
||||
this._modifiedSequence = modifiedSequence;
|
||||
|
||||
const [originalStringElements, originalElementsOrHash, originalHasStrings] = LcsDiff._getElements(originalSequence);
|
||||
const [modifiedStringElements, modifiedElementsOrHash, modifiedHasStrings] = LcsDiff._getElements(modifiedSequence);
|
||||
|
||||
@@ -288,6 +294,22 @@ export class LcsDiff {
|
||||
return (this._hasStrings ? this._originalStringElements[originalIndex] === this._modifiedStringElements[newIndex] : true);
|
||||
}
|
||||
|
||||
private ElementsAreStrictEqual(originalIndex: number, newIndex: number): boolean {
|
||||
if (!this.ElementsAreEqual(originalIndex, newIndex)) {
|
||||
return false;
|
||||
}
|
||||
const originalElement = LcsDiff._getStrictElement(this._originalSequence, originalIndex);
|
||||
const modifiedElement = LcsDiff._getStrictElement(this._modifiedSequence, newIndex);
|
||||
return (originalElement === modifiedElement);
|
||||
}
|
||||
|
||||
private static _getStrictElement(sequence: ISequence, index: number): string | null {
|
||||
if (typeof sequence.getStrictElement === 'function') {
|
||||
return sequence.getStrictElement(index);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private OriginalElementsAreEqual(index1: number, index2: number): boolean {
|
||||
if (this._originalElementsOrHash[index1] !== this._originalElementsOrHash[index2]) {
|
||||
return false;
|
||||
@@ -813,10 +835,18 @@ export class LcsDiff {
|
||||
const checkOriginal = change.originalLength > 0;
|
||||
const checkModified = change.modifiedLength > 0;
|
||||
|
||||
while (change.originalStart + change.originalLength < originalStop &&
|
||||
change.modifiedStart + change.modifiedLength < modifiedStop &&
|
||||
(!checkOriginal || this.OriginalElementsAreEqual(change.originalStart, change.originalStart + change.originalLength)) &&
|
||||
(!checkModified || this.ModifiedElementsAreEqual(change.modifiedStart, change.modifiedStart + change.modifiedLength))) {
|
||||
while (
|
||||
change.originalStart + change.originalLength < originalStop
|
||||
&& change.modifiedStart + change.modifiedLength < modifiedStop
|
||||
&& (!checkOriginal || this.OriginalElementsAreEqual(change.originalStart, change.originalStart + change.originalLength))
|
||||
&& (!checkModified || this.ModifiedElementsAreEqual(change.modifiedStart, change.modifiedStart + change.modifiedLength))
|
||||
) {
|
||||
const startStrictEqual = this.ElementsAreStrictEqual(change.originalStart, change.modifiedStart);
|
||||
const endStrictEqual = this.ElementsAreStrictEqual(change.originalStart + change.originalLength, change.modifiedStart + change.modifiedLength);
|
||||
if (endStrictEqual && !startStrictEqual) {
|
||||
// moving the change down would create an equal change, but the elements are not strict equal
|
||||
break;
|
||||
}
|
||||
change.originalStart++;
|
||||
change.modifiedStart++;
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ export namespace Event {
|
||||
* Given an event and a `filter` function, returns another event which emits those
|
||||
* elements for which the `filter` function returns `true`.
|
||||
*/
|
||||
export function filter<T, U>(event: Event<T | U>, filter: (e: T | U) => e is T): Event<T>;
|
||||
export function filter<T>(event: Event<T>, filter: (e: T) => boolean): Event<T>;
|
||||
export function filter<T, R>(event: Event<T | R>, filter: (e: T | R) => e is R): Event<R>;
|
||||
export function filter<T>(event: Event<T>, filter: (e: T) => boolean): Event<T> {
|
||||
@@ -188,18 +189,29 @@ export namespace Event {
|
||||
* Given an event, it returns another event which fires only when the event
|
||||
* element changes.
|
||||
*/
|
||||
export function latch<T>(event: Event<T>): Event<T> {
|
||||
export function latch<T>(event: Event<T>, equals: (a: T, b: T) => boolean = (a, b) => a === b): Event<T> {
|
||||
let firstCall = true;
|
||||
let cache: T;
|
||||
|
||||
return filter(event, value => {
|
||||
const shouldEmit = firstCall || value !== cache;
|
||||
const shouldEmit = firstCall || !equals(value, cache);
|
||||
firstCall = false;
|
||||
cache = value;
|
||||
return shouldEmit;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an event, it returns another event which fires only when the event
|
||||
* element changes.
|
||||
*/
|
||||
export function split<T, U>(event: Event<T | U>, isT: (e: T | U) => e is T): [Event<T>, Event<U>] {
|
||||
return [
|
||||
Event.filter(event, isT),
|
||||
Event.filter(event, e => !isT(e)) as Event<U>,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Buffers the provided event until a first listener comes
|
||||
* along, at which point fire all the events at once and
|
||||
@@ -633,10 +645,13 @@ export class Emitter<T> {
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._listeners?.clear();
|
||||
this._deliveryQueue?.clear();
|
||||
this._leakageMon?.dispose();
|
||||
this._disposed = true;
|
||||
if (!this._disposed) {
|
||||
this._disposed = true;
|
||||
this._listeners?.clear();
|
||||
this._deliveryQueue?.clear();
|
||||
this._options?.onLastListenerRemove?.();
|
||||
this._leakageMon?.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,23 @@ export function toSlashes(osPath: string) {
|
||||
return osPath.replace(/[\\/]/g, posix.sep);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a Windows OS path (using backward or forward slashes) and turns it into a posix path:
|
||||
* - turns backward slashes into forward slashes
|
||||
* - makes it absolute if it starts with a drive letter
|
||||
* This should only be done for OS paths from Windows (or user provided paths potentially from Windows).
|
||||
* Using it on a Linux or MaxOS path might change it.
|
||||
*/
|
||||
export function toPosixPath(osPath: string) {
|
||||
if (osPath.indexOf('/') === -1) {
|
||||
osPath = toSlashes(osPath);
|
||||
}
|
||||
if (/^[a-zA-Z]:(\/|$)/.test(osPath)) { // starts with a drive letter
|
||||
osPath = '/' + osPath;
|
||||
}
|
||||
return osPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the _root_ this path, like `getRoot('c:\files') === c:\`,
|
||||
* `getRoot('files:///files/path') === files:///`,
|
||||
|
||||
@@ -526,7 +526,7 @@ const enum Arrow { Diag = 1, Left = 2, LeftLeft = 3 }
|
||||
* 3. `<match_pos_1>`
|
||||
* 4. `<match_pos_0>` etc
|
||||
*/
|
||||
export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]];// [number, number, number];
|
||||
export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]];
|
||||
|
||||
export namespace FuzzyScore {
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
import { UriComponents } from 'vs/base/common/uri';
|
||||
import { escapeIcons } from 'vs/base/common/iconLabels';
|
||||
import { illegalArgument } from 'vs/base/common/errors';
|
||||
@@ -90,21 +89,7 @@ export function isMarkdownString(thing: any): thing is IMarkdownString {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function markedStringsEquals(a: IMarkdownString | IMarkdownString[], b: IMarkdownString | IMarkdownString[]): boolean {
|
||||
if (!a && !b) {
|
||||
return true;
|
||||
} else if (!a || !b) {
|
||||
return false;
|
||||
} else if (Array.isArray(a) && Array.isArray(b)) {
|
||||
return equals(a, b, markdownStringEqual);
|
||||
} else if (isMarkdownString(a) && isMarkdownString(b)) {
|
||||
return markdownStringEqual(a, b);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean {
|
||||
export function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
} else if (!a || !b) {
|
||||
|
||||
@@ -18,4 +18,4 @@ export class IdGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultGenerator = new IdGenerator('id#');
|
||||
export const defaultGenerator = new IdGenerator('id#');
|
||||
|
||||
@@ -30,7 +30,7 @@ export namespace Iterable {
|
||||
return iterable[Symbol.iterator]().next().value;
|
||||
}
|
||||
|
||||
export function some<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): boolean {
|
||||
export function some<T>(iterable: Iterable<T>, predicate: (t: T) => unknown): boolean {
|
||||
for (const element of iterable) {
|
||||
if (predicate(element)) {
|
||||
return true;
|
||||
@@ -61,9 +61,10 @@ export namespace Iterable {
|
||||
}
|
||||
}
|
||||
|
||||
export function* map<T, R>(iterable: Iterable<T>, fn: (t: T) => R): Iterable<R> {
|
||||
export function* map<T, R>(iterable: Iterable<T>, fn: (t: T, index: number) => R): Iterable<R> {
|
||||
let index = 0;
|
||||
for (const element of iterable) {
|
||||
yield fn(element);
|
||||
yield fn(element, index++);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,4 +23,4 @@ export function getParseErrorMessage(errorCode: ParseErrorCode): string {
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,4 +121,4 @@ export class KeybindingParser {
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +283,31 @@ export abstract class ReferenceCollection<T> {
|
||||
protected abstract destroyReferencedObject(key: string, object: T): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwraps a reference collection of promised values. Makes sure
|
||||
* references are disposed whenever promises get rejected.
|
||||
*/
|
||||
export class AsyncReferenceCollection<T> {
|
||||
|
||||
constructor(private referenceCollection: ReferenceCollection<Promise<T>>) { }
|
||||
|
||||
async acquire(key: string, ...args: any[]): Promise<IReference<T>> {
|
||||
const ref = this.referenceCollection.acquire(key, ...args);
|
||||
|
||||
try {
|
||||
const object = await ref.object;
|
||||
|
||||
return {
|
||||
object,
|
||||
dispose: () => ref.dispose()
|
||||
};
|
||||
} catch (error) {
|
||||
ref.dispose();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ImmortalReference<T> implements IReference<T> {
|
||||
constructor(public object: T) { }
|
||||
dispose(): void { /* noop */ }
|
||||
|
||||
@@ -33,6 +33,14 @@ export class LinkedList<E> {
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
let node = this._first;
|
||||
while (node !== Node.Undefined) {
|
||||
const next = node.next;
|
||||
node.prev = Node.Undefined;
|
||||
node.next = Node.Undefined;
|
||||
node = next;
|
||||
}
|
||||
|
||||
this._first = Node.Undefined;
|
||||
this._last = Node.Undefined;
|
||||
this._size = 0;
|
||||
|
||||
@@ -498,20 +498,27 @@ export class TernarySearchTree<K, V> {
|
||||
}
|
||||
|
||||
private *_entries(node: TernarySearchTreeNode<K, V> | undefined): IterableIterator<[K, V]> {
|
||||
if (node) {
|
||||
// left
|
||||
yield* this._entries(node.left);
|
||||
|
||||
// node
|
||||
if (node.value) {
|
||||
// callback(node.value, this._iter.join(parts));
|
||||
yield [node.key, node.value];
|
||||
// DFS
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const stack = [node];
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop();
|
||||
if (node) {
|
||||
if (node.value) {
|
||||
yield [node.key, node.value];
|
||||
}
|
||||
if (node.left) {
|
||||
stack.push(node.left);
|
||||
}
|
||||
if (node.mid) {
|
||||
stack.push(node.mid);
|
||||
}
|
||||
if (node.right) {
|
||||
stack.push(node.right);
|
||||
}
|
||||
}
|
||||
// mid
|
||||
yield* this._entries(node.mid);
|
||||
|
||||
// right
|
||||
yield* this._entries(node.right);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* marked - a markdown parser
|
||||
* Copyright (c) 2011-2020, Christopher Jeffrey. (Source EULAd)
|
||||
* Copyright (c) 2011-2021, Christopher Jeffrey. (Source EULAd)
|
||||
* https://github.com/markedjs/marked
|
||||
*/
|
||||
|
||||
|
||||
@@ -316,3 +316,20 @@ export function getExtensionForMimeType(mimeType: string): string | undefined {
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const _simplePattern = /^(.+)\/(.+?)(;.+)?$/;
|
||||
|
||||
export function normalizeMimeType(mimeType: string): string;
|
||||
export function normalizeMimeType(mimeType: string, strict: true): string | undefined;
|
||||
export function normalizeMimeType(mimeType: string, strict?: true): string | undefined {
|
||||
|
||||
const match = _simplePattern.exec(mimeType);
|
||||
if (!match) {
|
||||
return strict
|
||||
? undefined
|
||||
: mimeType;
|
||||
}
|
||||
// https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
|
||||
// media and subtype must ALWAYS be lowercase, parameter not
|
||||
return `${match[1].toLowerCase()}/${match[2].toLowerCase()}${match[3] ?? ''}`;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ export namespace Schemas {
|
||||
export const vscodeNotebookCell = 'vscode-notebook-cell';
|
||||
|
||||
export const vscodeNotebookCellMetadata = 'vscode-notebook-cell-metadata';
|
||||
export const vscodeNotebookCellOutput = 'vscode-notebook-cell-output';
|
||||
|
||||
export const vscodeSettings = 'vscode-settings';
|
||||
|
||||
|
||||
@@ -113,18 +113,6 @@ export function mixin(destination: any, source: any, overwrite: boolean = true):
|
||||
return destination;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated ES6
|
||||
*/
|
||||
export function assign<T>(destination: T): T;
|
||||
export function assign<T, U>(destination: T, u: U): T & U;
|
||||
export function assign<T, U, V>(destination: T, u: U, v: V): T & U & V;
|
||||
export function assign<T, U, V, W>(destination: T, u: U, v: V, w: W): T & U & V & W;
|
||||
export function assign(destination: any, ...sources: any[]): any {
|
||||
sources.forEach(source => Object.keys(source).forEach(key => destination[key] = source[key]));
|
||||
return destination;
|
||||
}
|
||||
|
||||
export function equals(one: any, other: any): boolean {
|
||||
if (one === other) {
|
||||
return true;
|
||||
@@ -238,3 +226,13 @@ export function getCaseInsensitive(target: obj, key: string): any {
|
||||
const equivalentKey = Object.keys(target).find(k => k.toLowerCase() === lowercaseKey);
|
||||
return equivalentKey ? target[equivalentKey] : target[key];
|
||||
}
|
||||
|
||||
export function filter(obj: obj, predicate: (key: string, value: any) => boolean): obj {
|
||||
const result = Object.create(null);
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (predicate(key, obj[key])) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -76,4 +76,4 @@ export abstract class Parser {
|
||||
public fatal(message: string): void {
|
||||
this._problemReporter.fatal(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ declare const self: unknown;
|
||||
export const globals: any = (typeof self === 'object' ? self : typeof global === 'object' ? global : {});
|
||||
|
||||
let nodeProcess: INodeProcess | undefined = undefined;
|
||||
if (typeof globals.vscode !== 'undefined') {
|
||||
if (typeof globals.vscode !== 'undefined' && typeof globals.vscode.process !== 'undefined') {
|
||||
// Native environment (sandboxed)
|
||||
nodeProcess = globals.vscode.process;
|
||||
} else if (typeof process !== 'undefined') {
|
||||
|
||||
13
src/vs/base/common/ports.ts
Normal file
13
src/vs/base/common/ports.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* @returns Returns a random port between 1025 and 65535.
|
||||
*/
|
||||
export function randomPort(): number {
|
||||
const min = 1025;
|
||||
const max = 65535;
|
||||
return min + Math.floor((max - min) * Math.random());
|
||||
}
|
||||
@@ -9,7 +9,7 @@ let safeProcess: INodeProcess & { nextTick: (callback: (...args: any[]) => void)
|
||||
declare const process: INodeProcess;
|
||||
|
||||
// Native sandbox environment
|
||||
if (typeof globals.vscode !== 'undefined') {
|
||||
if (typeof globals.vscode !== 'undefined' && typeof globals.vscode.process !== 'undefined') {
|
||||
const sandboxProcess: INodeProcess = globals.vscode.process;
|
||||
safeProcess = {
|
||||
get platform() { return sandboxProcess.platform; },
|
||||
@@ -38,7 +38,7 @@ else {
|
||||
nextTick(callback: (...args: any[]) => void): void { return setImmediate(callback); },
|
||||
|
||||
// Unsupported
|
||||
get env() { return Object.create(null); },
|
||||
get env() { return {}; },
|
||||
cwd() { return '/'; }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
@@ -25,6 +25,11 @@ export type ExtensionUntrustedWorkspaceSupport = {
|
||||
readonly override?: boolean | 'limited'
|
||||
};
|
||||
|
||||
export type ExtensionVirtualWorkspaceSupport = {
|
||||
readonly default?: boolean,
|
||||
readonly override?: boolean
|
||||
};
|
||||
|
||||
export interface IProductConfiguration {
|
||||
readonly version: string;
|
||||
readonly date?: string;
|
||||
@@ -74,6 +79,7 @@ export interface IProductConfiguration {
|
||||
readonly remoteExtensionTips?: { [remoteName: string]: IRemoteExtensionTip; };
|
||||
readonly extensionKeywords?: { [extension: string]: readonly string[]; };
|
||||
readonly keymapExtensionTips?: readonly string[];
|
||||
readonly languageExtensionTips?: readonly string[];
|
||||
readonly trustedExtensionUrlPublicKeys?: { [id: string]: string[]; };
|
||||
|
||||
readonly recommendedExtensions: string[]; // {{SQL CARBON EDIT}}
|
||||
@@ -128,7 +134,7 @@ export interface IProductConfiguration {
|
||||
readonly extensionSyncedKeys?: { readonly [extensionId: string]: string[]; };
|
||||
readonly extensionAllowedProposedApi?: readonly string[];
|
||||
readonly extensionUntrustedWorkspaceSupport?: { readonly [extensionId: string]: ExtensionUntrustedWorkspaceSupport };
|
||||
readonly extensionVirtualWorkspacesSupport?: { readonly [extensionId: string]: { default?: boolean, override?: boolean } };
|
||||
readonly extensionVirtualWorkspacesSupport?: { readonly [extensionId: string]: ExtensionVirtualWorkspaceSupport };
|
||||
|
||||
readonly msftInternalDomains?: string[];
|
||||
readonly linkProtectionTrustedDomains?: readonly string[];
|
||||
@@ -136,6 +142,8 @@ export interface IProductConfiguration {
|
||||
readonly 'configurationSync.store'?: ConfigurationSyncStore;
|
||||
|
||||
readonly darwinUniversalAssetId?: string;
|
||||
|
||||
readonly webviewContentExternalBaseUrlTemplate?: string;
|
||||
}
|
||||
|
||||
export type ImportantExtensionTip = { name: string; languages?: string[]; pattern?: string; isExtensionPack?: boolean };
|
||||
|
||||
@@ -50,7 +50,7 @@ export interface IExtUri {
|
||||
|
||||
/**
|
||||
* Creates a key from a resource URI to be used to resource comparison and for resource maps.
|
||||
* @see ResourceMap
|
||||
* @see {@link ResourceMap}
|
||||
* @param uri Uri
|
||||
* @param ignoreFragment Ignore the fragment (defaults to `false`)
|
||||
*/
|
||||
@@ -264,12 +264,7 @@ export class ExtUri implements IExtUri {
|
||||
path: newURI.path
|
||||
});
|
||||
}
|
||||
if (path.indexOf('/') === -1) { // no slashes? it's likely a Windows path
|
||||
path = extpath.toSlashes(path);
|
||||
if (/^[a-zA-Z]:(\/|$)/.test(path)) { // starts with a drive letter
|
||||
path = '/' + path;
|
||||
}
|
||||
}
|
||||
path = extpath.toPosixPath(path); // we allow path to be a windows path
|
||||
return base.with({
|
||||
path: paths.posix.resolve(base.path, path)
|
||||
});
|
||||
|
||||
@@ -152,41 +152,6 @@ export function stripWildcards(pattern: string): string {
|
||||
return pattern.replace(/\*/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated ES6: use `String.startsWith`
|
||||
*/
|
||||
export function startsWith(haystack: string, needle: string): boolean {
|
||||
if (haystack.length < needle.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (haystack === needle) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let i = 0; i < needle.length; i++) {
|
||||
if (haystack[i] !== needle[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated ES6: use `String.endsWith`
|
||||
*/
|
||||
export function endsWith(haystack: string, needle: string): boolean {
|
||||
const diff = haystack.length - needle.length;
|
||||
if (diff > 0) {
|
||||
return haystack.indexOf(needle, diff) === diff;
|
||||
} else if (diff === 0) {
|
||||
return haystack === needle;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export interface RegExpOptions {
|
||||
matchCase?: boolean;
|
||||
wholeWord?: boolean;
|
||||
@@ -1129,3 +1094,81 @@ function getGraphemeBreakRawData(): number[] {
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
/**
|
||||
* Computes the offset after performing a left delete on the given string,
|
||||
* while considering unicode grapheme/emoji rules.
|
||||
*/
|
||||
export function getLeftDeleteOffset(offset: number, str: string): number {
|
||||
if (offset === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Try to delete emoji part.
|
||||
const emojiOffset = getOffsetBeforeLastEmojiComponent(offset, str);
|
||||
if (emojiOffset !== undefined) {
|
||||
return emojiOffset;
|
||||
}
|
||||
|
||||
// Otherwise, just skip a single code point.
|
||||
const codePoint = getPrevCodePoint(str, offset);
|
||||
offset -= getUTF16Length(codePoint);
|
||||
return offset;
|
||||
}
|
||||
|
||||
function getOffsetBeforeLastEmojiComponent(offset: number, str: string): number | undefined {
|
||||
// See https://www.unicode.org/reports/tr51/tr51-14.html#EBNF_and_Regex for the
|
||||
// structure of emojis.
|
||||
let codePoint = getPrevCodePoint(str, offset);
|
||||
offset -= getUTF16Length(codePoint);
|
||||
|
||||
// Skip modifiers
|
||||
while ((isEmojiModifier(codePoint) || codePoint === CodePoint.emojiVariantSelector || codePoint === CodePoint.enclosingKeyCap)) {
|
||||
if (offset === 0) {
|
||||
// Cannot skip modifier, no preceding emoji base.
|
||||
return undefined;
|
||||
}
|
||||
codePoint = getPrevCodePoint(str, offset);
|
||||
offset -= getUTF16Length(codePoint);
|
||||
}
|
||||
|
||||
// Expect base emoji
|
||||
if (!isEmojiImprecise(codePoint)) {
|
||||
// Unexpected code point, not a valid emoji.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (offset >= 0) {
|
||||
// Skip optional ZWJ code points that combine multiple emojis.
|
||||
// In theory, we should check if that ZWJ actually combines multiple emojis
|
||||
// to prevent deleting ZWJs in situations we didn't account for.
|
||||
const optionalZwjCodePoint = getPrevCodePoint(str, offset);
|
||||
if (optionalZwjCodePoint === CodePoint.zwj) {
|
||||
offset -= getUTF16Length(optionalZwjCodePoint);
|
||||
}
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
function getUTF16Length(codePoint: number) {
|
||||
return codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1;
|
||||
}
|
||||
|
||||
function isEmojiModifier(codePoint: number): boolean {
|
||||
return 0x1F3FB <= codePoint && codePoint <= 0x1F3FF;
|
||||
}
|
||||
|
||||
const enum CodePoint {
|
||||
zwj = 0x200D,
|
||||
|
||||
/**
|
||||
* Variation Selector-16 (VS16)
|
||||
*/
|
||||
emojiVariantSelector = 0xFE0F,
|
||||
|
||||
/**
|
||||
* Combining Enclosing Keycap
|
||||
*/
|
||||
enclosingKeyCap = 0x20E3,
|
||||
}
|
||||
|
||||
@@ -327,13 +327,15 @@ export class URI implements UriComponents {
|
||||
}
|
||||
|
||||
static from(components: { scheme: string; authority?: string; path?: string; query?: string; fragment?: string }): URI {
|
||||
return new Uri(
|
||||
const result = new Uri(
|
||||
components.scheme,
|
||||
components.authority,
|
||||
components.path,
|
||||
components.query,
|
||||
components.fragment,
|
||||
);
|
||||
_validateUri(result, true);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -152,4 +152,4 @@ export function transformAndReviveIncomingURIs<T>(obj: T, transformer: IURITrans
|
||||
return obj;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import { rtrim } from 'vs/base/common/strings';
|
||||
import { sep, join, normalize, dirname, basename } from 'vs/base/common/path';
|
||||
import { readdirSync } from 'vs/base/node/pfs';
|
||||
import { Promises, readdirSync } from 'vs/base/node/pfs';
|
||||
|
||||
/**
|
||||
* Copied from: https://github.com/microsoft/vscode-node-debug/blob/master/src/node/pathUtilities.ts#L83
|
||||
@@ -57,7 +56,7 @@ export async function realpath(path: string): Promise<string> {
|
||||
// calls `fs.native.realpath` which will result in subst
|
||||
// drives to be resolved to their target on Windows
|
||||
// https://github.com/microsoft/vscode/issues/118562
|
||||
return await promisify(fs.realpath)(path);
|
||||
return await Promises.realpath(path);
|
||||
} catch (error) {
|
||||
|
||||
// We hit an error calling fs.realpath(). Since fs.realpath() is doing some path normalization
|
||||
@@ -67,7 +66,7 @@ export async function realpath(path: string): Promise<string> {
|
||||
// to not resolve links but to simply see if the path is read accessible or not.
|
||||
const normalizedPath = normalizePath(path);
|
||||
|
||||
await fs.promises.access(normalizedPath, fs.constants.R_OK);
|
||||
await Promises.access(normalizedPath, fs.constants.R_OK);
|
||||
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { promisify } from 'util';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { ResourceQueue } from 'vs/base/common/async';
|
||||
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
|
||||
@@ -38,7 +39,7 @@ export enum RimRafMode {
|
||||
* - `MOVE`: faster variant that first moves the target to temp dir and then
|
||||
* deletes it in the background without waiting for that to finish.
|
||||
*/
|
||||
export async function rimraf(path: string, mode = RimRafMode.UNLINK): Promise<void> {
|
||||
async function rimraf(path: string, mode = RimRafMode.UNLINK): Promise<void> {
|
||||
if (isRootOrDriveLetter(path)) {
|
||||
throw new Error('rimraf - will refuse to recursively delete root');
|
||||
}
|
||||
@@ -56,7 +57,7 @@ async function rimrafMove(path: string): Promise<void> {
|
||||
try {
|
||||
const pathInTemp = join(tmpdir(), generateUuid());
|
||||
try {
|
||||
await fs.promises.rename(path, pathInTemp);
|
||||
await Promises.rename(path, pathInTemp);
|
||||
} catch (error) {
|
||||
return rimrafUnlink(path); // if rename fails, delete without tmp dir
|
||||
}
|
||||
@@ -71,7 +72,7 @@ async function rimrafMove(path: string): Promise<void> {
|
||||
}
|
||||
|
||||
async function rimrafUnlink(path: string): Promise<void> {
|
||||
return fs.promises.rmdir(path, { recursive: true, maxRetries: 3 });
|
||||
return Promises.rmdir(path, { recursive: true, maxRetries: 3 });
|
||||
}
|
||||
|
||||
export function rimrafSync(path: string): void {
|
||||
@@ -99,15 +100,15 @@ export interface IDirent {
|
||||
* for converting from macOS NFD unicon form to NFC
|
||||
* (https://github.com/nodejs/node/issues/2165)
|
||||
*/
|
||||
export async function readdir(path: string): Promise<string[]>;
|
||||
export async function readdir(path: string, options: { withFileTypes: true }): Promise<IDirent[]>;
|
||||
export async function readdir(path: string, options?: { withFileTypes: true }): Promise<(string | IDirent)[]> {
|
||||
return handleDirectoryChildren(await (options ? safeReaddirWithFileTypes(path) : fs.promises.readdir(path)));
|
||||
async function readdir(path: string): Promise<string[]>;
|
||||
async function readdir(path: string, options: { withFileTypes: true }): Promise<IDirent[]>;
|
||||
async function readdir(path: string, options?: { withFileTypes: true }): Promise<(string | IDirent)[]> {
|
||||
return handleDirectoryChildren(await (options ? safeReaddirWithFileTypes(path) : promisify(fs.readdir)(path)));
|
||||
}
|
||||
|
||||
async function safeReaddirWithFileTypes(path: string): Promise<IDirent[]> {
|
||||
try {
|
||||
return await fs.promises.readdir(path, { withFileTypes: true });
|
||||
return await promisify(fs.readdir)(path, { withFileTypes: true });
|
||||
} catch (error) {
|
||||
console.warn('[node.js fs] readdir with filetypes failed with error: ', error);
|
||||
}
|
||||
@@ -126,7 +127,7 @@ async function safeReaddirWithFileTypes(path: string): Promise<IDirent[]> {
|
||||
let isSymbolicLink = false;
|
||||
|
||||
try {
|
||||
const lstat = await fs.promises.lstat(join(path, child));
|
||||
const lstat = await Promises.lstat(join(path, child));
|
||||
|
||||
isFile = lstat.isFile();
|
||||
isDirectory = lstat.isDirectory();
|
||||
@@ -178,7 +179,7 @@ function handleDirectoryChildren(children: (string | IDirent)[]): (string | IDir
|
||||
* A convinience method to read all children of a path that
|
||||
* are directories.
|
||||
*/
|
||||
export async function readDirsInDir(dirPath: string): Promise<string[]> {
|
||||
async function readDirsInDir(dirPath: string): Promise<string[]> {
|
||||
const children = await readdir(dirPath);
|
||||
const directories: string[] = [];
|
||||
|
||||
@@ -251,7 +252,7 @@ export namespace SymlinkSupport {
|
||||
// First stat the link
|
||||
let lstats: fs.Stats | undefined;
|
||||
try {
|
||||
lstats = await fs.promises.lstat(path);
|
||||
lstats = await Promises.lstat(path);
|
||||
|
||||
// Return early if the stat is not a symbolic link at all
|
||||
if (!lstats.isSymbolicLink()) {
|
||||
@@ -264,7 +265,7 @@ export namespace SymlinkSupport {
|
||||
// If the stat is a symbolic link or failed to stat, use fs.stat()
|
||||
// which for symbolic links will stat the target they point to
|
||||
try {
|
||||
const stats = await fs.promises.stat(path);
|
||||
const stats = await Promises.stat(path);
|
||||
|
||||
return { stat: stats, symbolicLink: lstats?.isSymbolicLink() ? { dangling: false } : undefined };
|
||||
} catch (error) {
|
||||
@@ -279,7 +280,7 @@ export namespace SymlinkSupport {
|
||||
// are not supported (https://github.com/nodejs/node/issues/36790)
|
||||
if (isWindows && error.code === 'EACCES') {
|
||||
try {
|
||||
const stats = await fs.promises.stat(await fs.promises.readlink(path));
|
||||
const stats = await Promises.stat(await Promises.readlink(path));
|
||||
|
||||
return { stat: stats, symbolicLink: { dangling: false } };
|
||||
} catch (error) {
|
||||
@@ -359,11 +360,11 @@ const writeQueues = new ResourceQueue();
|
||||
*
|
||||
* In addition, multiple writes to the same path are queued.
|
||||
*/
|
||||
export function writeFile(path: string, data: string, options?: IWriteFileOptions): Promise<void>;
|
||||
export function writeFile(path: string, data: Buffer, options?: IWriteFileOptions): Promise<void>;
|
||||
export function writeFile(path: string, data: Uint8Array, options?: IWriteFileOptions): Promise<void>;
|
||||
export function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise<void>;
|
||||
export function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise<void> {
|
||||
function writeFile(path: string, data: string, options?: IWriteFileOptions): Promise<void>;
|
||||
function writeFile(path: string, data: Buffer, options?: IWriteFileOptions): Promise<void>;
|
||||
function writeFile(path: string, data: Uint8Array, options?: IWriteFileOptions): Promise<void>;
|
||||
function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise<void>;
|
||||
function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise<void> {
|
||||
return writeQueues.queueFor(URI.file(path), extUriBiasedIgnorePathCase).queue(() => {
|
||||
const ensuredOptions = ensureWriteOptions(options);
|
||||
|
||||
@@ -371,7 +372,7 @@ export function writeFile(path: string, data: string | Buffer | Uint8Array, opti
|
||||
});
|
||||
}
|
||||
|
||||
export interface IWriteFileOptions {
|
||||
interface IWriteFileOptions {
|
||||
mode?: number;
|
||||
flag?: string;
|
||||
}
|
||||
@@ -473,7 +474,7 @@ function ensureWriteOptions(options?: IWriteFileOptions): IEnsuredWriteFileOptio
|
||||
* - updates the `mtime` of the `source` after the operation
|
||||
* - allows to move across multiple disks
|
||||
*/
|
||||
export async function move(source: string, target: string): Promise<void> {
|
||||
async function move(source: string, target: string): Promise<void> {
|
||||
if (source === target) {
|
||||
return; // simulate node.js behaviour here and do a no-op if paths match
|
||||
}
|
||||
@@ -488,24 +489,19 @@ export async function move(source: string, target: string): Promise<void> {
|
||||
// as well because conceptually it is a change of a similar category.
|
||||
async function updateMtime(path: string): Promise<void> {
|
||||
try {
|
||||
const stat = await fs.promises.lstat(path);
|
||||
const stat = await Promises.lstat(path);
|
||||
if (stat.isDirectory() || stat.isSymbolicLink()) {
|
||||
return; // only for files
|
||||
}
|
||||
|
||||
const fh = await fs.promises.open(path, 'a');
|
||||
try {
|
||||
await fh.utimes(stat.atime, new Date());
|
||||
} finally {
|
||||
await fh.close();
|
||||
}
|
||||
await Promises.utimes(path, stat.atime, new Date());
|
||||
} catch (error) {
|
||||
// Ignore any error
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.promises.rename(source, target);
|
||||
await Promises.rename(source, target);
|
||||
await updateMtime(target);
|
||||
} catch (error) {
|
||||
|
||||
@@ -540,7 +536,7 @@ interface ICopyPayload {
|
||||
* links should be handled when encountered. Set to
|
||||
* `false` to not preserve them and `true` otherwise.
|
||||
*/
|
||||
export async function copy(source: string, target: string, options: { preserveSymlinks: boolean }): Promise<void> {
|
||||
async function copy(source: string, target: string, options: { preserveSymlinks: boolean }): Promise<void> {
|
||||
return doCopy(source, target, { root: { source, target }, options, handledSourcePaths: new Set<string>() });
|
||||
}
|
||||
|
||||
@@ -594,7 +590,7 @@ async function doCopy(source: string, target: string, payload: ICopyPayload): Pr
|
||||
async function doCopyDirectory(source: string, target: string, mode: number, payload: ICopyPayload): Promise<void> {
|
||||
|
||||
// Create folder
|
||||
await fs.promises.mkdir(target, { recursive: true, mode });
|
||||
await Promises.mkdir(target, { recursive: true, mode });
|
||||
|
||||
// Copy each file recursively
|
||||
const files = await readdir(source);
|
||||
@@ -606,16 +602,16 @@ async function doCopyDirectory(source: string, target: string, mode: number, pay
|
||||
async function doCopyFile(source: string, target: string, mode: number): Promise<void> {
|
||||
|
||||
// Copy file
|
||||
await fs.promises.copyFile(source, target);
|
||||
await Promises.copyFile(source, target);
|
||||
|
||||
// restore mode (https://github.com/nodejs/node/issues/1104)
|
||||
await fs.promises.chmod(target, mode);
|
||||
await Promises.chmod(target, mode);
|
||||
}
|
||||
|
||||
async function doCopySymlink(source: string, target: string, payload: ICopyPayload): Promise<void> {
|
||||
|
||||
// Figure out link target
|
||||
let linkTarget = await fs.promises.readlink(source);
|
||||
let linkTarget = await Promises.readlink(source);
|
||||
|
||||
// Special case: the symlink points to a target that is
|
||||
// actually within the path that is being copied. In that
|
||||
@@ -626,21 +622,92 @@ async function doCopySymlink(source: string, target: string, payload: ICopyPaylo
|
||||
}
|
||||
|
||||
// Create symlink
|
||||
await fs.promises.symlink(linkTarget, target);
|
||||
await Promises.symlink(linkTarget, target);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Async FS Methods
|
||||
//#region Promise based fs methods
|
||||
|
||||
export async function exists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.promises.access(path);
|
||||
/**
|
||||
* Prefer this helper class over the `fs.promises` API to
|
||||
* enable `graceful-fs` to function properly. Given issue
|
||||
* https://github.com/isaacs/node-graceful-fs/issues/160 it
|
||||
* is evident that the module only takes care of the non-promise
|
||||
* based fs methods.
|
||||
*
|
||||
* Another reason is `realpath` being entirely different in
|
||||
* the promise based implementation compared to the other
|
||||
* one (https://github.com/microsoft/vscode/issues/118562)
|
||||
*
|
||||
* Note: using getters for a reason, since `graceful-fs`
|
||||
* patching might kick in later after modules have been
|
||||
* loaded we need to defer access to fs methods.
|
||||
* (https://github.com/microsoft/vscode/issues/124176)
|
||||
*/
|
||||
export const Promises = new class {
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
//#region Implemented by node.js
|
||||
|
||||
get access() { return promisify(fs.access); }
|
||||
|
||||
get stat() { return promisify(fs.stat); }
|
||||
get lstat() { return promisify(fs.lstat); }
|
||||
get utimes() { return promisify(fs.utimes); }
|
||||
|
||||
get read() { return promisify(fs.read); }
|
||||
get readFile() { return promisify(fs.readFile); }
|
||||
|
||||
get write() { return promisify(fs.write); }
|
||||
|
||||
get appendFile() { return promisify(fs.appendFile); }
|
||||
|
||||
get fdatasync() { return promisify(fs.fdatasync); }
|
||||
get truncate() { return promisify(fs.truncate); }
|
||||
|
||||
get rename() { return promisify(fs.rename); }
|
||||
get copyFile() { return promisify(fs.copyFile); }
|
||||
|
||||
get open() { return promisify(fs.open); }
|
||||
get close() { return promisify(fs.close); }
|
||||
|
||||
get symlink() { return promisify(fs.symlink); }
|
||||
get readlink() { return promisify(fs.readlink); }
|
||||
|
||||
get chmod() { return promisify(fs.chmod); }
|
||||
|
||||
get mkdir() { return promisify(fs.mkdir); }
|
||||
|
||||
get unlink() { return promisify(fs.unlink); }
|
||||
get rmdir() { return promisify(fs.rmdir); }
|
||||
|
||||
get realpath() { return promisify(fs.realpath); }
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Implemented by us
|
||||
|
||||
async exists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await Promises.access(path);
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get readdir() { return readdir; }
|
||||
get readDirsInDir() { return readDirsInDir; }
|
||||
|
||||
get writeFile() { return writeFile; }
|
||||
|
||||
get rm() { return rimraf; }
|
||||
|
||||
get move() { return move; }
|
||||
get copy() { return copy; }
|
||||
|
||||
//#endregion
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -5,15 +5,6 @@
|
||||
|
||||
import * as net from 'net';
|
||||
|
||||
/**
|
||||
* @returns Returns a random port between 1025 and 65535.
|
||||
*/
|
||||
export function randomPort(): number {
|
||||
const min = 1025;
|
||||
const max = 65535;
|
||||
return min + Math.floor((max - min) * Math.random());
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a start point and a max number of retries, will find a port that
|
||||
* is openable. Will return 0 in case no free port can be found.
|
||||
|
||||
@@ -137,7 +137,7 @@ async function findPSCoreWindowsInstallation(
|
||||
|
||||
let highestSeenVersion: number = -1;
|
||||
let pwshExePath: string | null = null;
|
||||
for (const item of await pfs.readdir(powerShellInstallBaseDir)) {
|
||||
for (const item of await pfs.Promises.readdir(powerShellInstallBaseDir)) {
|
||||
|
||||
let currentVersion: number = -1;
|
||||
if (findPreview) {
|
||||
@@ -210,7 +210,7 @@ async function findPSCoreMsix({ findPreview }: { findPreview?: boolean } = {}):
|
||||
: { 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)) {
|
||||
for (const subdir of await pfs.Promises.readdir(msixAppDir)) {
|
||||
if (pwshMsixDirRegex.test(subdir)) {
|
||||
const pwshMsixPath = path.join(msixAppDir, subdir, 'pwsh.exe');
|
||||
return new PossiblePowerShellExe(pwshMsixPath, pwshMsixName);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as fs from 'fs';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import * as cp from 'child_process';
|
||||
import * as nls from 'vs/nls';
|
||||
@@ -457,8 +456,8 @@ export namespace win32 {
|
||||
}
|
||||
|
||||
async function fileExists(path: string): Promise<boolean> {
|
||||
if (await pfs.exists(path)) {
|
||||
return !((await fs.promises.stat(path)).isDirectory());
|
||||
if (await pfs.Promises.exists(path)) {
|
||||
return !((await pfs.Promises.stat(path)).isDirectory());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { watch } from 'fs';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { normalizeNFC } from 'vs/base/common/normalization';
|
||||
import { toDisposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { exists, readdir } from 'vs/base/node/pfs';
|
||||
import { Promises } from 'vs/base/node/pfs';
|
||||
|
||||
export function watchFile(path: string, onChange: (type: 'added' | 'changed' | 'deleted', path: string) => void, onError: (error: string) => void): IDisposable {
|
||||
return doWatchNonRecursive({ path, isDirectory: false }, onChange, onError);
|
||||
@@ -42,7 +42,7 @@ function doWatchNonRecursive(file: { path: string, isDirectory: boolean }, onCha
|
||||
// Folder: resolve children to emit proper events
|
||||
const folderChildren: Set<string> = new Set<string>();
|
||||
if (file.isDirectory) {
|
||||
readdir(file.path).then(children => children.forEach(child => folderChildren.add(child)));
|
||||
Promises.readdir(file.path).then(children => children.forEach(child => folderChildren.add(child)));
|
||||
}
|
||||
|
||||
watcher.on('error', (code: number, signal: string) => {
|
||||
@@ -87,7 +87,7 @@ function doWatchNonRecursive(file: { path: string, isDirectory: boolean }, onCha
|
||||
// does indeed not exist anymore.
|
||||
|
||||
const timeoutHandle = setTimeout(async () => {
|
||||
const fileExists = await exists(changedFilePath);
|
||||
const fileExists = await Promises.exists(changedFilePath);
|
||||
|
||||
if (disposed) {
|
||||
return; // ignore if disposed by now
|
||||
@@ -131,7 +131,7 @@ function doWatchNonRecursive(file: { path: string, isDirectory: boolean }, onCha
|
||||
const timeoutHandle = setTimeout(async () => {
|
||||
mapPathToStatDisposable.delete(changedFilePath);
|
||||
|
||||
const fileExists = await exists(changedFilePath);
|
||||
const fileExists = await Promises.exists(changedFilePath);
|
||||
|
||||
if (disposed) {
|
||||
return; // ignore if disposed by now
|
||||
@@ -177,7 +177,7 @@ function doWatchNonRecursive(file: { path: string, isDirectory: boolean }, onCha
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
exists(file.path).then(exists => {
|
||||
Promises.exists(file.path).then(exists => {
|
||||
if (exists && !disposed) {
|
||||
onError(`Failed to watch ${file.path} for changes using fs.watch() (${error.toString()})`);
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import { promises, createWriteStream, WriteStream } from 'fs';
|
||||
import { createWriteStream, WriteStream } from 'fs';
|
||||
import { Readable } from 'stream';
|
||||
import { Sequencer, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { rimraf } from 'vs/base/node/pfs';
|
||||
import { Promises } from 'vs/base/node/pfs';
|
||||
import { open as _openZip, Entry, ZipFile } from 'yauzl';
|
||||
import * as yazl from 'yazl';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
@@ -86,7 +86,7 @@ function extractEntry(stream: Readable, fileName: string, mode: number, targetPa
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.resolve(promises.mkdir(targetDirName, { recursive: true })).then(() => new Promise<void>((c, e) => {
|
||||
return Promise.resolve(Promises.mkdir(targetDirName, { recursive: true })).then(() => new Promise<void>((c, e) => {
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
@@ -149,7 +149,7 @@ function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, tok
|
||||
// directory file names end with '/'
|
||||
if (/\/$/.test(fileName)) {
|
||||
const targetFileName = path.join(targetPath, fileName);
|
||||
last = createCancelablePromise(token => promises.mkdir(targetFileName, { recursive: true }).then(() => readNextEntry(token)).then(undefined, e));
|
||||
last = createCancelablePromise(token => Promises.mkdir(targetFileName, { recursive: true }).then(() => readNextEntry(token)).then(undefined, e));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ export function extract(zipPath: string, targetPath: string, options: IExtractOp
|
||||
let promise = openZip(zipPath, true);
|
||||
|
||||
if (options.overwrite) {
|
||||
promise = promise.then(zipfile => rimraf(targetPath).then(() => zipfile));
|
||||
promise = promise.then(zipfile => Promises.rm(targetPath).then(() => zipfile));
|
||||
}
|
||||
|
||||
return promise.then(zipfile => extractZip(zipfile, targetPath, { sourcePathRegex }, token));
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getRandomElement } from 'vs/base/common/arrays';
|
||||
import { isFunction, isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { revive } from 'vs/base/common/marshalling';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
|
||||
/**
|
||||
* An `IChannel` is an abstraction over a collection of commands.
|
||||
@@ -723,11 +724,16 @@ export class ChannelClient implements IChannelClient, IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
@memoize
|
||||
get onDidInitializePromise(): Promise<void> {
|
||||
return Event.toPromise(this.onDidInitialize);
|
||||
}
|
||||
|
||||
private whenInitialized(): Promise<void> {
|
||||
if (this.state === State.Idle) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Event.toPromise(this.onDidInitialize);
|
||||
return this.onDidInitializePromise;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,4 +8,4 @@ import { TestChannel, TestService } from './testService';
|
||||
|
||||
const server = new Server('test');
|
||||
const service = new TestService();
|
||||
server.registerChannel('test', new TestChannel(service));
|
||||
server.registerChannel('test', new TestChannel(service));
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
border-top-style: solid;
|
||||
}
|
||||
|
||||
.quick-input-list .monaco-list-row:first-child .quick-input-list-entry.quick-input-list-separator-border {
|
||||
.quick-input-list .monaco-list-row[data-index="0"] .quick-input-list-entry.quick-input-list-separator-border {
|
||||
border-top-style: none;
|
||||
}
|
||||
|
||||
@@ -276,3 +276,14 @@
|
||||
.quick-input-list .monaco-list-row.focused .quick-input-list-entry-action-bar .action-label {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* focused items in quick pick */
|
||||
.quick-input-list .monaco-list-row.focused .monaco-keybinding-key,
|
||||
.quick-input-list .monaco-list-row.focused .quick-input-list-entry .quick-input-list-separator,
|
||||
.quick-input-list .monaco-list-row.focused .quick-input-list-rows .quick-input-list-row .codicon,
|
||||
.quick-input-list .monaco-list-row.focused .quick-input-list-entry-action-bar .codicon {
|
||||
color: inherit
|
||||
}
|
||||
.quick-input-list .monaco-list-row.focused .monaco-keybinding-key {
|
||||
background: none;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/quickInput';
|
||||
import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent, NO_KEY_MODS, ItemActivation, QuickInputHideReason, IQuickInputHideEvent } from 'vs/base/parts/quickinput/common/quickInput';
|
||||
import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods, IQuickPickDidAcceptEvent, NO_KEY_MODS, ItemActivation, QuickInputHideReason, IQuickInputHideEvent, IQuickPickWillAcceptEvent } from 'vs/base/parts/quickinput/common/quickInput';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { QuickInputList, QuickInputListFocus } from './quickInputList';
|
||||
@@ -29,7 +29,6 @@ import { IInputBoxStyles } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
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 { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
|
||||
import { isString } from 'vs/base/common/types';
|
||||
import { IKeybindingLabelStyles } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel';
|
||||
@@ -359,7 +358,7 @@ class QuickInput extends Disposable implements IQuickInput {
|
||||
const validationMessage = this.validationMessage || this.noValidationMessage;
|
||||
if (this._lastValidationMessage !== validationMessage) {
|
||||
this._lastValidationMessage = validationMessage;
|
||||
dom.reset(this.ui.message, ...renderLabelWithIcons(escape(validationMessage)));
|
||||
dom.reset(this.ui.message, ...renderLabelWithIcons(validationMessage));
|
||||
}
|
||||
if (this._lastSeverity !== this.severity) {
|
||||
this._lastSeverity = this.severity;
|
||||
@@ -428,7 +427,8 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
|
||||
private _ariaLabel: string | undefined;
|
||||
private _placeholder: string | undefined;
|
||||
private readonly onDidChangeValueEmitter = this._register(new Emitter<string>());
|
||||
private readonly onDidAcceptEmitter = this._register(new Emitter<IQuickPickAcceptEvent>());
|
||||
private readonly onWillAcceptEmitter = this._register(new Emitter<IQuickPickWillAcceptEvent>());
|
||||
private readonly onDidAcceptEmitter = this._register(new Emitter<IQuickPickDidAcceptEvent>());
|
||||
private readonly onDidCustomEmitter = this._register(new Emitter<void>());
|
||||
private _items: Array<T | IQuickPickSeparator> = [];
|
||||
private itemsUpdated = false;
|
||||
@@ -473,8 +473,11 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
|
||||
}
|
||||
|
||||
set value(value: string) {
|
||||
this._value = value || '';
|
||||
this.update();
|
||||
if (this._value !== value) {
|
||||
this._value = value || '';
|
||||
this.update();
|
||||
this.onDidChangeValueEmitter.fire(this._value);
|
||||
}
|
||||
}
|
||||
|
||||
filterValue = (value: string) => value;
|
||||
@@ -499,6 +502,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
|
||||
|
||||
onDidChangeValue = this.onDidChangeValueEmitter.event;
|
||||
|
||||
onWillAccept = this.onWillAcceptEmitter.event;
|
||||
onDidAccept = this.onDidAcceptEmitter.event;
|
||||
|
||||
onDidCustom = this.onDidCustomEmitter.event;
|
||||
@@ -761,7 +765,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
|
||||
if (this.activeItems[0]) {
|
||||
this._selectedItems = [this.activeItems[0]];
|
||||
this.onDidChangeSelectionEmitter.fire(this.selectedItems);
|
||||
this.onDidAcceptEmitter.fire({ inBackground: true });
|
||||
this.handleAccept(true);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -784,7 +788,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
|
||||
this._selectedItems = [this.activeItems[0]];
|
||||
this.onDidChangeSelectionEmitter.fire(this.selectedItems);
|
||||
}
|
||||
this.onDidAcceptEmitter.fire({ inBackground: false });
|
||||
this.handleAccept(false);
|
||||
}));
|
||||
this.visibleDisposables.add(this.ui.onDidCustom(() => {
|
||||
this.onDidCustomEmitter.fire();
|
||||
@@ -812,7 +816,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
|
||||
this._selectedItems = selectedItems as T[];
|
||||
this.onDidChangeSelectionEmitter.fire(selectedItems as T[]);
|
||||
if (selectedItems.length) {
|
||||
this.onDidAcceptEmitter.fire({ inBackground: event instanceof MouseEvent && event.button === 1 /* mouse middle click */ });
|
||||
this.handleAccept(event instanceof MouseEvent && event.button === 1 /* mouse middle click */);
|
||||
}
|
||||
}));
|
||||
this.visibleDisposables.add(this.ui.list.onChangedCheckedElements(checkedItems => {
|
||||
@@ -832,6 +836,18 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
|
||||
super.show(); // TODO: Why have show() bubble up while update() trickles down? (Could move setComboboxAccessibility() here.)
|
||||
}
|
||||
|
||||
private handleAccept(inBackground: boolean): void {
|
||||
|
||||
// Figure out veto via `onWillAccept` event
|
||||
let veto = false;
|
||||
this.onWillAcceptEmitter.fire({ veto: () => veto = true });
|
||||
|
||||
// Continue with `onDidAccpet` if no veto
|
||||
if (!veto) {
|
||||
this.onDidAcceptEmitter.fire({ inBackground });
|
||||
}
|
||||
}
|
||||
|
||||
private registerQuickNavigation() {
|
||||
return dom.addDisposableListener(this.ui.container, dom.EventType.KEY_UP, e => {
|
||||
if (this.canSelectMany || !this._quickNavigate) {
|
||||
@@ -876,7 +892,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
|
||||
if (this.activeItems[0]) {
|
||||
this._selectedItems = [this.activeItems[0]];
|
||||
this.onDidChangeSelectionEmitter.fire(this.selectedItems);
|
||||
this.onDidAcceptEmitter.fire({ inBackground: false });
|
||||
this.handleAccept(false);
|
||||
}
|
||||
// Unset quick navigate after press. It is only valid once
|
||||
// and should not result in any behaviour change afterwards
|
||||
|
||||
@@ -207,7 +207,17 @@ export interface IQuickInput extends IDisposable {
|
||||
hide(): void;
|
||||
}
|
||||
|
||||
export interface IQuickPickAcceptEvent {
|
||||
export interface IQuickPickWillAcceptEvent {
|
||||
|
||||
/**
|
||||
* Allows to disable the default accept handling
|
||||
* of the picker. If `veto` is called, the picker
|
||||
* will not trigger the `onDidAccept` event.
|
||||
*/
|
||||
veto(): void;
|
||||
}
|
||||
|
||||
export interface IQuickPickDidAcceptEvent {
|
||||
|
||||
/**
|
||||
* Signals if the picker item is to be accepted
|
||||
@@ -239,7 +249,8 @@ export interface IQuickPick<T extends IQuickPickItem> extends IQuickInput {
|
||||
|
||||
readonly onDidChangeValue: Event<string>;
|
||||
|
||||
readonly onDidAccept: Event<IQuickPickAcceptEvent>;
|
||||
readonly onWillAccept: Event<IQuickPickWillAcceptEvent>;
|
||||
readonly onDidAccept: Event<IQuickPickDidAcceptEvent>;
|
||||
|
||||
/**
|
||||
* If enabled, will fire the `onDidAccept` event when
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
|
||||
|
||||
export interface MessageBoxOptions {
|
||||
/**
|
||||
* Content of the message box.
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Can be `"none"`, `"info"`, `"error"`, `"question"` or `"warning"`. On Windows,
|
||||
* `"question"` displays the same icon as `"info"`, unless you set an icon using
|
||||
@@ -34,10 +38,6 @@ export interface MessageBoxOptions {
|
||||
* Title of the message box, some platforms will not show it.
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* Content of the message box.
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Extra information of the message.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IProcessEnvironment } from 'vs/base/common/platform';
|
||||
@@ -46,7 +46,7 @@ export interface ISandboxConfiguration {
|
||||
zoomLevel?: number;
|
||||
|
||||
/**
|
||||
* @deprecated to be removed soon
|
||||
* Location of V8 code cache.
|
||||
*/
|
||||
nodeCachedDataDir?: string;
|
||||
codeCachePath?: string;
|
||||
}
|
||||
|
||||
@@ -112,12 +112,6 @@
|
||||
ipcRenderer.invoke('vscode:fetchShellEnv')
|
||||
]);
|
||||
|
||||
if (!process.env['VSCODE_SKIP_PROCESS_ENV_PATCHING'] /* TODO@bpasero for https://github.com/microsoft/vscode/issues/108804 */) {
|
||||
// Assign all keys of the shell environment to our process environment
|
||||
// But make sure that the user environment wins in the end over shell environment
|
||||
Object.assign(process.env, shellEnv, userEnv);
|
||||
}
|
||||
|
||||
return { ...process.env, ...shellEnv, ...userEnv };
|
||||
})();
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ export interface IpcRendererEvent extends Event {
|
||||
}
|
||||
|
||||
export interface IpcRenderer {
|
||||
|
||||
// Docs: https://electronjs.org/docs/api/ipc-renderer
|
||||
|
||||
/**
|
||||
* Listens to `channel`, when a new message arrives `listener` would be called with
|
||||
* `listener(event, args...)`.
|
||||
@@ -58,9 +61,13 @@ export interface IpcRenderer {
|
||||
* 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.
|
||||
* > **NOTE:** Sending non-standard JavaScript types such as DOM objects or special
|
||||
* Electron objects will throw an exception.
|
||||
*
|
||||
* Since the main process does not have support for DOM objects such as
|
||||
* `ImageBitmap`, `File`, `DOMMatrix` and so on, such objects cannot be sent over
|
||||
* Electron's IPC to the main process, as the main process would have no way to
|
||||
* decode them. Attempting to send such objects over IPC will result in an error.
|
||||
*
|
||||
* The main process handles it by listening for `channel` with the `ipcMain`
|
||||
* module.
|
||||
@@ -81,9 +88,13 @@ export interface IpcRenderer {
|
||||
* 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.
|
||||
* > **NOTE:** Sending non-standard JavaScript types such as DOM objects or special
|
||||
* Electron objects will throw an exception.
|
||||
*
|
||||
* Since the main process does not have support for DOM objects such as
|
||||
* `ImageBitmap`, `File`, `DOMMatrix` and so on, such objects cannot be sent over
|
||||
* Electron's IPC to the main process, as the main process would have no way to
|
||||
* decode them. Attempting to send such objects over IPC will result in an error.
|
||||
*
|
||||
* The main process should listen for `channel` with `ipcMain.handle()`.
|
||||
*
|
||||
@@ -111,7 +122,7 @@ export interface IpcRenderer {
|
||||
// * For more information on using `MessagePort` and `MessageChannel`, see the MDN
|
||||
// * documentation.
|
||||
// */
|
||||
// postMessage(channel: string, message: any): void;
|
||||
// postMessage(channel: string, message: any, transfer?: MessagePort[]): void;
|
||||
}
|
||||
|
||||
export interface WebFrame {
|
||||
@@ -119,6 +130,11 @@ export interface WebFrame {
|
||||
* Changes the zoom level to the specified level. The original size is 0 and each
|
||||
* increment above or below represents zooming 20% larger or smaller to default
|
||||
* limits of 300% and 50% of original size, respectively.
|
||||
*
|
||||
* > **NOTE**: The zoom policy at the Chromium level is same-origin, meaning that
|
||||
* the zoom level for a specific domain propagates across all instances of windows
|
||||
* with the same domain. Differentiating the window URLs will make zoom work
|
||||
* per-window.
|
||||
*/
|
||||
setZoomLevel(level: number): void;
|
||||
}
|
||||
@@ -207,7 +223,7 @@ export interface CrashReporterStartOptions {
|
||||
rateLimit?: boolean;
|
||||
/**
|
||||
* If true, crash reports will be compressed and uploaded with `Content-Encoding:
|
||||
* gzip`. Default is `false`.
|
||||
* gzip`. Default is `true`.
|
||||
*/
|
||||
compress?: boolean;
|
||||
/**
|
||||
|
||||
@@ -4,12 +4,11 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type { Database, Statement } from 'vscode-sqlite3';
|
||||
import { promises } from 'fs';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { mapToString, setToString } from 'vs/base/common/map';
|
||||
import { basename } from 'vs/base/common/path';
|
||||
import { copy } from 'vs/base/node/pfs';
|
||||
import { Promises } from 'vs/base/node/pfs';
|
||||
import { IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage';
|
||||
|
||||
interface IDatabaseConnection {
|
||||
@@ -187,7 +186,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
|
||||
// Delete the existing DB. If the path does not exist or fails to
|
||||
// be deleted, we do not try to recover anymore because we assume
|
||||
// that the path is no longer writeable for us.
|
||||
return promises.unlink(this.path).then(() => {
|
||||
return Promises.unlink(this.path).then(() => {
|
||||
|
||||
// Re-open the DB fresh
|
||||
return this.doConnect(this.path).then(recoveryConnection => {
|
||||
@@ -217,7 +216,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
|
||||
private backup(): Promise<void> {
|
||||
const backupPath = this.toBackupPath(this.path);
|
||||
|
||||
return copy(this.path, backupPath, { preserveSymlinks: false });
|
||||
return Promises.copy(this.path, backupPath, { preserveSymlinks: false });
|
||||
}
|
||||
|
||||
private toBackupPath(path: string): string {
|
||||
@@ -273,9 +272,9 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
|
||||
// folder is really not writeable for us.
|
||||
//
|
||||
try {
|
||||
await promises.unlink(path);
|
||||
await Promises.unlink(path);
|
||||
try {
|
||||
await promises.rename(this.toBackupPath(path), path);
|
||||
await Promises.rename(this.toBackupPath(path), path);
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
@@ -7,9 +7,8 @@ import { SQLiteStorageDatabase, ISQLiteStorageDatabaseOptions } from 'vs/base/pa
|
||||
import { Storage, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/parts/storage/common/storage';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { tmpdir } from 'os';
|
||||
import { promises } from 'fs';
|
||||
import { strictEqual, ok } from 'assert';
|
||||
import { writeFile, exists, rimraf } from 'vs/base/node/pfs';
|
||||
import { Promises } from 'vs/base/node/pfs';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
@@ -23,11 +22,11 @@ flakySuite('Storage Library', function () {
|
||||
setup(function () {
|
||||
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storagelibrary');
|
||||
|
||||
return promises.mkdir(testDir, { recursive: true });
|
||||
return Promises.mkdir(testDir, { recursive: true });
|
||||
});
|
||||
|
||||
teardown(function () {
|
||||
return rimraf(testDir);
|
||||
return Promises.rm(testDir);
|
||||
});
|
||||
|
||||
test('basics', async () => {
|
||||
@@ -214,7 +213,7 @@ flakySuite('Storage Library', function () {
|
||||
});
|
||||
|
||||
test.skip('conflicting updates', async () => { // {{SQL CARBON EDIT}} test is disabled due to failures
|
||||
let storage = new Storage(new SQLiteStorageDatabase(join('storageDir', 'storage.db')));
|
||||
let storage = new Storage(new SQLiteStorageDatabase(join(testDir, 'storage.db')));
|
||||
await storage.init();
|
||||
|
||||
let changes = new Set<string>();
|
||||
@@ -263,7 +262,7 @@ flakySuite('Storage Library', function () {
|
||||
|
||||
await storage.set('bar', 'foo');
|
||||
|
||||
await writeFile(storageFile, 'This is a broken DB');
|
||||
await Promises.writeFile(storageFile, 'This is a broken DB');
|
||||
|
||||
await storage.set('foo', 'bar');
|
||||
|
||||
@@ -296,11 +295,11 @@ flakySuite('SQLite Storage Library', function () {
|
||||
setup(function () {
|
||||
testdir = getRandomTestPath(tmpdir(), 'vsctests', 'storagelibrary');
|
||||
|
||||
return promises.mkdir(testdir, { recursive: true });
|
||||
return Promises.mkdir(testdir, { recursive: true });
|
||||
});
|
||||
|
||||
teardown(function () {
|
||||
return rimraf(testdir);
|
||||
return Promises.rm(testdir);
|
||||
});
|
||||
|
||||
async function testDBBasics(path: string, logError?: (error: Error | string) => void) {
|
||||
@@ -386,7 +385,7 @@ flakySuite('SQLite Storage Library', function () {
|
||||
|
||||
test('basics (corrupt DB falls back to empty DB)', async () => {
|
||||
const corruptDBPath = join(testdir, 'broken.db');
|
||||
await writeFile(corruptDBPath, 'This is a broken DB');
|
||||
await Promises.writeFile(corruptDBPath, 'This is a broken DB');
|
||||
|
||||
let expectedError: any;
|
||||
await testDBBasics(corruptDBPath, error => {
|
||||
@@ -408,7 +407,7 @@ flakySuite('SQLite Storage Library', function () {
|
||||
await storage.updateItems({ insert: items });
|
||||
await storage.close();
|
||||
|
||||
await writeFile(storagePath, 'This is now a broken DB');
|
||||
await Promises.writeFile(storagePath, 'This is now a broken DB');
|
||||
|
||||
storage = new SQLiteStorageDatabase(storagePath);
|
||||
|
||||
@@ -440,8 +439,8 @@ flakySuite('SQLite Storage Library', function () {
|
||||
await storage.updateItems({ insert: items });
|
||||
await storage.close();
|
||||
|
||||
await writeFile(storagePath, 'This is now a broken DB');
|
||||
await writeFile(`${storagePath}.backup`, 'This is now also a broken DB');
|
||||
await Promises.writeFile(storagePath, 'This is now a broken DB');
|
||||
await Promises.writeFile(`${storagePath}.backup`, 'This is now also a broken DB');
|
||||
|
||||
storage = new SQLiteStorageDatabase(storagePath);
|
||||
|
||||
@@ -464,12 +463,12 @@ flakySuite('SQLite Storage Library', function () {
|
||||
await storage.close();
|
||||
|
||||
const backupPath = `${storagePath}.backup`;
|
||||
strictEqual(await exists(backupPath), true);
|
||||
strictEqual(await Promises.exists(backupPath), true);
|
||||
|
||||
storage = new SQLiteStorageDatabase(storagePath);
|
||||
await storage.getItems();
|
||||
|
||||
await writeFile(storagePath, 'This is now a broken DB');
|
||||
await Promises.writeFile(storagePath, 'This is now a broken DB');
|
||||
|
||||
// we still need to trigger a check to the DB so that we get to know that
|
||||
// the DB is corrupt. We have no extra code on shutdown that checks for the
|
||||
@@ -477,7 +476,7 @@ flakySuite('SQLite Storage Library', function () {
|
||||
// on shutdown.
|
||||
await storage.checkIntegrity(true).then(null, error => { } /* error is expected here but we do not want to fail */);
|
||||
|
||||
await promises.unlink(backupPath); // also test that the recovery DB is backed up properly
|
||||
await Promises.unlink(backupPath); // also test that the recovery DB is backed up properly
|
||||
|
||||
let recoveryCalled = false;
|
||||
await storage.close(() => {
|
||||
@@ -487,7 +486,7 @@ flakySuite('SQLite Storage Library', function () {
|
||||
});
|
||||
|
||||
strictEqual(recoveryCalled, true);
|
||||
strictEqual(await exists(backupPath), true);
|
||||
strictEqual(await Promises.exists(backupPath), true);
|
||||
|
||||
storage = new SQLiteStorageDatabase(storagePath);
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ export class DefaultController implements _.IController {
|
||||
protected isClickOnTwistie(event: mouse.IMouseEvent): boolean {
|
||||
let element = event.target as HTMLElement;
|
||||
|
||||
if (!dom.hasClass(element, 'content')) {
|
||||
if (!element.classList.contains('content')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -493,11 +493,11 @@ export class TreeView extends HeightMap {
|
||||
}
|
||||
|
||||
if (this.context.options.alwaysFocused) {
|
||||
DOM.addClass(this.domNode, 'focused');
|
||||
this.domNode.classList.add('focused');
|
||||
}
|
||||
|
||||
if (!this.context.options.paddingOnRow) {
|
||||
DOM.addClass(this.domNode, 'no-row-padding');
|
||||
this.domNode.classList.add('no-row-padding');
|
||||
}
|
||||
|
||||
this.wrapper = document.createElement('div');
|
||||
@@ -1023,7 +1023,7 @@ export class TreeView extends HeightMap {
|
||||
viewItem.addClass(trait);
|
||||
}
|
||||
if (trait === 'highlighted') {
|
||||
DOM.addClass(this.domNode, trait);
|
||||
this.domNode.classList.add(trait);
|
||||
|
||||
// Ugly Firefox fix: input fields can't be selected if parent nodes are draggable
|
||||
if (viewItem) {
|
||||
@@ -1043,7 +1043,7 @@ export class TreeView extends HeightMap {
|
||||
viewItem.removeClass(trait);
|
||||
}
|
||||
if (trait === 'highlighted') {
|
||||
DOM.removeClass(this.domNode, trait);
|
||||
this.domNode.classList.remove(trait);
|
||||
|
||||
// Ugly Firefox fix: input fields can't be selected if parent nodes are draggable
|
||||
if (this.highlightedItemWasDraggable) {
|
||||
@@ -1056,7 +1056,7 @@ export class TreeView extends HeightMap {
|
||||
private onModelFocusChange(): void {
|
||||
const focus = this.model && this.model.getFocus();
|
||||
|
||||
DOM.toggleClass(this.domNode, 'no-focused-item', !focus);
|
||||
this.domNode.classList.toggle('no-focused-item', !focus);
|
||||
|
||||
// ARIA
|
||||
if (focus) {
|
||||
@@ -1512,7 +1512,7 @@ export class TreeView extends HeightMap {
|
||||
|
||||
private onFocus(): void {
|
||||
if (!this.context.options.alwaysFocused) {
|
||||
DOM.addClass(this.domNode, 'focused');
|
||||
this.domNode.classList.add('focused');
|
||||
}
|
||||
|
||||
this._onDOMFocus.fire();
|
||||
@@ -1520,7 +1520,7 @@ export class TreeView extends HeightMap {
|
||||
|
||||
private onBlur(): void {
|
||||
if (!this.context.options.alwaysFocused) {
|
||||
DOM.removeClass(this.domNode, 'focused');
|
||||
this.domNode.classList.remove('focused');
|
||||
}
|
||||
|
||||
this.domNode.removeAttribute('aria-activedescendant'); // ARIA
|
||||
|
||||
@@ -3,13 +3,23 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { compareFileNames, compareFileExtensions, compareFileNamesDefault, compareFileExtensionsDefault } from 'vs/base/common/comparers';
|
||||
import {
|
||||
compareFileNames,
|
||||
compareFileExtensions,
|
||||
compareFileNamesDefault,
|
||||
compareFileExtensionsDefault,
|
||||
compareFileNamesUpper,
|
||||
compareFileExtensionsUpper,
|
||||
compareFileNamesLower,
|
||||
compareFileExtensionsLower,
|
||||
compareFileNamesUnicode,
|
||||
compareFileExtensionsUnicode,
|
||||
} from 'vs/base/common/comparers';
|
||||
import * as assert from 'assert';
|
||||
|
||||
const compareLocale = (a: string, b: string) => a.localeCompare(b);
|
||||
const compareLocaleNumeric = (a: string, b: string) => a.localeCompare(b, undefined, { numeric: true });
|
||||
|
||||
|
||||
suite('Comparers', () => {
|
||||
|
||||
test('compareFileNames', () => {
|
||||
@@ -23,11 +33,11 @@ suite('Comparers', () => {
|
||||
assert(compareFileNames(null, 'abc') < 0, 'null should be come before real values');
|
||||
assert(compareFileNames('', '') === 0, 'empty should be equal');
|
||||
assert(compareFileNames('abc', 'abc') === 0, 'equal names should be equal');
|
||||
assert(compareFileNames('z', 'A') > 0, 'z comes is after A regardless of case');
|
||||
assert(compareFileNames('Z', 'a') > 0, 'Z comes after a regardless of case');
|
||||
assert(compareFileNames('z', 'A') > 0, 'z comes after A');
|
||||
assert(compareFileNames('Z', 'a') > 0, 'Z comes after a');
|
||||
|
||||
// name plus extension comparisons
|
||||
assert(compareFileNames('bbb.aaa', 'aaa.bbb') > 0, 'files with extensions are compared first by filename');
|
||||
assert(compareFileNames('bbb.aaa', 'aaa.bbb') > 0, 'compares the whole name all at once by locale');
|
||||
assert(compareFileNames('aggregate.go', 'aggregate_repo.go') > 0, 'compares the whole name all at once by locale');
|
||||
|
||||
// dotfile comparisons
|
||||
@@ -35,7 +45,7 @@ suite('Comparers', () => {
|
||||
assert(compareFileNames('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly');
|
||||
assert(compareFileNames('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots');
|
||||
assert(compareFileNames('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first');
|
||||
assert(compareFileNames('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot');
|
||||
assert(compareFileNames('.aaa_env', '.aaa.env') < 0, 'an underscore in a dotfile name will sort before a dot');
|
||||
|
||||
// dotfile vs non-dotfile comparisons
|
||||
assert(compareFileNames(null, '.abc') < 0, 'null should come before dotfiles');
|
||||
@@ -51,14 +61,16 @@ suite('Comparers', () => {
|
||||
assert(compareFileNames('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long');
|
||||
assert(compareFileNames('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically');
|
||||
assert(compareFileNames('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number');
|
||||
assert(compareFileNames('a.ext1', 'b.Ext1') < 0, 'if names are different and extensions with numbers are equal except for case, filenames are sorted in name order');
|
||||
assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileNames), ['A2.txt', 'a10.txt', 'a20.txt', 'A100.txt'], 'filenames with number and case differences compare numerically');
|
||||
|
||||
//
|
||||
// Comparisons with different results than compareFileNamesDefault
|
||||
//
|
||||
|
||||
// name-only comparisons
|
||||
assert(compareFileNames('a', 'A') !== compareLocale('a', 'A'), 'the same letter does not sort by locale');
|
||||
assert(compareFileNames('â', 'Â') !== compareLocale('â', 'Â'), 'the same accented letter does not sort by locale');
|
||||
assert(compareFileNames('a', 'A') !== compareLocale('a', 'A'), 'the same letter sorts in unicode order, not by locale');
|
||||
assert(compareFileNames('â', 'Â') !== compareLocale('â', 'Â'), 'the same accented letter sorts in unicode order, not by locale');
|
||||
assert.notDeepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNames), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases do not sort in locale order');
|
||||
assert.notDeepStrictEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNames), ['email', 'Email', 'émail', 'Émail'].sort(compareLocale), 'the same base characters with different case or accents do not sort in locale order');
|
||||
|
||||
@@ -67,6 +79,7 @@ suite('Comparers', () => {
|
||||
assert(compareFileNames('abc.txt1', 'abc.txt01') > 0, 'same name plus extensions with equal numbers sort in unicode order');
|
||||
assert(compareFileNames('art01', 'Art01') !== 'art01'.localeCompare('Art01', undefined, { numeric: true }),
|
||||
'a numerically equivalent word of a different case does not compare numerically based on locale');
|
||||
assert(compareFileNames('a.ext1', 'a.Ext1') > 0, 'if names are equal and extensions with numbers are equal except for case, filenames are sorted in full filename unicode order');
|
||||
|
||||
});
|
||||
|
||||
@@ -89,9 +102,6 @@ suite('Comparers', () => {
|
||||
assert(compareFileExtensions('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared');
|
||||
assert(compareFileExtensions('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions');
|
||||
assert(compareFileExtensions('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extensions even if filenames compare differently');
|
||||
assert(compareFileExtensions('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names');
|
||||
assert(compareFileExtensions('agg.go', 'agg_repo.go') < 0, 'shorter names short before longer names even when the longer name contains an underscore');
|
||||
assert(compareFileExtensions('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name');
|
||||
|
||||
// dotfile comparisons
|
||||
assert(compareFileExtensions('.abc', '.abc') === 0, 'equal dotfiles should be equal');
|
||||
@@ -113,8 +123,8 @@ suite('Comparers', () => {
|
||||
assert(compareFileExtensions('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal');
|
||||
assert(compareFileExtensions('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order');
|
||||
assert(compareFileExtensions('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long');
|
||||
assert(compareFileExtensions('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared');
|
||||
assert(compareFileExtensions('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically');
|
||||
assert(compareFileExtensions('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, names should be compared');
|
||||
assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileExtensions), ['A2.txt', 'a10.txt', 'a20.txt', 'A100.txt'], 'filenames with number and case differences compare numerically');
|
||||
|
||||
//
|
||||
// Comparisons with different results from compareFileExtensionsDefault
|
||||
@@ -127,8 +137,9 @@ suite('Comparers', () => {
|
||||
assert.notDeepStrictEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensions), ['email', 'Email', 'émail', 'Émail'].sort((a, b) => a.localeCompare(b)), 'the same base characters with different case or accents do not sort in locale order');
|
||||
|
||||
// name plus extension comparisons
|
||||
assert(compareFileExtensions('a.MD', 'a.md') !== compareLocale('MD', 'md'), 'case differences in extensions do not sort by locale');
|
||||
assert(compareFileExtensions('a.md', 'A.md') !== compareLocale('a', 'A'), 'case differences in names do not sort by locale');
|
||||
assert(compareFileExtensions('a.MD', 'a.md') < 0, 'case differences in extensions sort in unicode order');
|
||||
assert(compareFileExtensions('a.md', 'A.md') > 0, 'case differences in names sort in unicode order');
|
||||
assert(compareFileExtensions('a.md', 'b.MD') > 0, 'when extensions are the same except for case, the files sort by extension');
|
||||
assert(compareFileExtensions('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, names sort in dictionary order');
|
||||
|
||||
// dotfile comparisons
|
||||
@@ -144,6 +155,8 @@ suite('Comparers', () => {
|
||||
assert(compareFileExtensions('art01', 'Art01') !== compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case does not compare by locale');
|
||||
assert(compareFileExtensions('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort in unicode order');
|
||||
assert(compareFileExtensions('txt.abc01', 'txt.abc1') < 0, 'extensions with equivalent numbers sort in unicode order');
|
||||
assert(compareFileExtensions('a.ext1', 'b.Ext1') > 0, 'if names are different and extensions with numbers are equal except for case, filenames are sorted in extension unicode order');
|
||||
assert(compareFileExtensions('a.ext1', 'a.Ext1') > 0, 'if names are equal and extensions with numbers are equal except for case, filenames are sorted in extension unicode order');
|
||||
|
||||
});
|
||||
|
||||
@@ -158,8 +171,8 @@ suite('Comparers', () => {
|
||||
assert(compareFileNamesDefault(null, 'abc') < 0, 'null should be come before real values');
|
||||
assert(compareFileNamesDefault('', '') === 0, 'empty should be equal');
|
||||
assert(compareFileNamesDefault('abc', 'abc') === 0, 'equal names should be equal');
|
||||
assert(compareFileNamesDefault('z', 'A') > 0, 'z comes is after A regardless of case');
|
||||
assert(compareFileNamesDefault('Z', 'a') > 0, 'Z comes after a regardless of case');
|
||||
assert(compareFileNamesDefault('z', 'A') > 0, 'z comes after A');
|
||||
assert(compareFileNamesDefault('Z', 'a') > 0, 'Z comes after a');
|
||||
|
||||
// name plus extension comparisons
|
||||
assert(compareFileNamesDefault('file.ext', 'file.ext') === 0, 'equal full names should be equal');
|
||||
@@ -173,7 +186,7 @@ suite('Comparers', () => {
|
||||
assert(compareFileNamesDefault('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly');
|
||||
assert(compareFileNamesDefault('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots');
|
||||
assert(compareFileNamesDefault('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first');
|
||||
assert(compareFileNamesDefault('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot');
|
||||
assert(compareFileNamesDefault('.aaa_env', '.aaa.env') < 0, 'an underscore in a dotfile name will sort before a dot');
|
||||
|
||||
// dotfile vs non-dotfile comparisons
|
||||
assert(compareFileNamesDefault(null, '.abc') < 0, 'null should come before dotfiles');
|
||||
@@ -189,6 +202,8 @@ suite('Comparers', () => {
|
||||
assert(compareFileNamesDefault('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long');
|
||||
assert(compareFileNamesDefault('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically');
|
||||
assert(compareFileNamesDefault('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number');
|
||||
assert(compareFileNamesDefault('a.ext1', 'b.Ext1') < 0, 'if names are different and extensions with numbers are equal except for case, filenames are compared by full filename');
|
||||
assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileNamesDefault), ['A2.txt', 'a10.txt', 'a20.txt', 'A100.txt'], 'filenames with number and case differences compare numerically');
|
||||
|
||||
//
|
||||
// Comparisons with different results than compareFileNames
|
||||
@@ -203,7 +218,7 @@ suite('Comparers', () => {
|
||||
assert(compareFileNamesDefault('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first');
|
||||
assert(compareFileNamesDefault('abc.txt1', 'abc.txt01') < 0, 'same name plus extensions with equal numbers sort shortest number first');
|
||||
assert(compareFileNamesDefault('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale');
|
||||
|
||||
assert(compareFileNamesDefault('a.ext1', 'a.Ext1') === compareLocale('ext1', 'Ext1'), 'if names are equal and extensions with numbers are equal except for case, filenames are sorted in extension locale order');
|
||||
});
|
||||
|
||||
test('compareFileExtensionsDefault', () => {
|
||||
@@ -225,8 +240,6 @@ suite('Comparers', () => {
|
||||
assert(compareFileExtensionsDefault('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared');
|
||||
assert(compareFileExtensionsDefault('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions');
|
||||
assert(compareFileExtensionsDefault('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first');
|
||||
assert(compareFileExtensionsDefault('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names');
|
||||
assert(compareFileExtensionsDefault('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name');
|
||||
|
||||
// dotfile comparisons
|
||||
assert(compareFileExtensionsDefault('.abc', '.abc') === 0, 'equal dotfiles should be equal');
|
||||
@@ -248,8 +261,8 @@ suite('Comparers', () => {
|
||||
assert(compareFileExtensionsDefault('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal');
|
||||
assert(compareFileExtensionsDefault('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order');
|
||||
assert(compareFileExtensionsDefault('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long');
|
||||
assert(compareFileExtensionsDefault('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared');
|
||||
assert(compareFileExtensionsDefault('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically');
|
||||
assert(compareFileExtensionsDefault('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, full filenames should be compared');
|
||||
assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileExtensionsDefault), ['A2.txt', 'a10.txt', 'a20.txt', 'A100.txt'], 'filenames with number and case differences compare numerically');
|
||||
|
||||
//
|
||||
// Comparisons with different results than compareFileExtensions
|
||||
@@ -263,6 +276,7 @@ suite('Comparers', () => {
|
||||
// name plus extension comparisons
|
||||
assert(compareFileExtensionsDefault('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale');
|
||||
assert(compareFileExtensionsDefault('a.md', 'A.md') === compareLocale('a', 'A'), 'case differences in names sort by locale');
|
||||
assert(compareFileExtensionsDefault('a.md', 'b.MD') < 0, 'when extensions are the same except for case, the files sort by name');
|
||||
assert(compareFileExtensionsDefault('aggregate.go', 'aggregate_repo.go') > 0, 'names with the same extension sort in full filename locale order');
|
||||
|
||||
// dotfile comparisons
|
||||
@@ -278,6 +292,418 @@ suite('Comparers', () => {
|
||||
assert(compareFileExtensionsDefault('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale');
|
||||
assert(compareFileExtensionsDefault('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first');
|
||||
assert(compareFileExtensionsDefault('txt.abc01', 'txt.abc1') > 0, 'extensions with equivalent numbers sort shortest extension first');
|
||||
assert(compareFileExtensionsDefault('a.ext1', 'b.Ext1') < 0, 'if extensions with numbers are equal except for case, full filenames should be compared');
|
||||
assert(compareFileExtensionsDefault('a.ext1', 'a.Ext1') === compareLocale('a.ext1', 'a.Ext1'), 'if extensions with numbers are equal except for case, full filenames are compared in locale order');
|
||||
|
||||
});
|
||||
|
||||
test('compareFileNamesUpper', () => {
|
||||
|
||||
//
|
||||
// Comparisons with the same results as compareFileNamesDefault
|
||||
//
|
||||
|
||||
// name-only comparisons
|
||||
assert(compareFileNamesUpper(null, null) === 0, 'null should be equal');
|
||||
assert(compareFileNamesUpper(null, 'abc') < 0, 'null should be come before real values');
|
||||
assert(compareFileNamesUpper('', '') === 0, 'empty should be equal');
|
||||
assert(compareFileNamesUpper('abc', 'abc') === 0, 'equal names should be equal');
|
||||
assert(compareFileNamesUpper('z', 'A') > 0, 'z comes after A');
|
||||
|
||||
// name plus extension comparisons
|
||||
assert(compareFileNamesUpper('file.ext', 'file.ext') === 0, 'equal full names should be equal');
|
||||
assert(compareFileNamesUpper('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared');
|
||||
assert(compareFileNamesUpper('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions');
|
||||
assert(compareFileNamesUpper('bbb.aaa', 'aaa.bbb') > 0, 'files should be compared by names even if extensions compare differently');
|
||||
assert(compareFileNamesUpper('aggregate.go', 'aggregate_repo.go') > 0, 'compares the full filename in locale order');
|
||||
|
||||
// dotfile comparisons
|
||||
assert(compareFileNamesUpper('.abc', '.abc') === 0, 'equal dotfile names should be equal');
|
||||
assert(compareFileNamesUpper('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly');
|
||||
assert(compareFileNamesUpper('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots');
|
||||
assert(compareFileNamesUpper('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first');
|
||||
assert(compareFileNamesUpper('.aaa_env', '.aaa.env') < 0, 'an underscore in a dotfile name will sort before a dot');
|
||||
|
||||
// dotfile vs non-dotfile comparisons
|
||||
assert(compareFileNamesUpper(null, '.abc') < 0, 'null should come before dotfiles');
|
||||
assert(compareFileNamesUpper('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions');
|
||||
assert(compareFileNamesUpper('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions');
|
||||
assert(compareFileNamesUpper('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files');
|
||||
assert(compareFileNamesUpper('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files');
|
||||
|
||||
// numeric comparisons
|
||||
assert(compareFileNamesUpper('1', '1') === 0, 'numerically equal full names should be equal');
|
||||
assert(compareFileNamesUpper('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal');
|
||||
assert(compareFileNamesUpper('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order');
|
||||
assert(compareFileNamesUpper('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long');
|
||||
assert(compareFileNamesUpper('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically');
|
||||
assert(compareFileNamesUpper('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number');
|
||||
assert(compareFileNamesUpper('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first');
|
||||
assert(compareFileNamesUpper('abc.txt1', 'abc.txt01') < 0, 'same name plus extensions with equal numbers sort shortest number first');
|
||||
assert(compareFileNamesUpper('a.ext1', 'b.Ext1') < 0, 'different names with the equal extensions except for case are sorted by full filename');
|
||||
assert(compareFileNamesUpper('a.ext1', 'a.Ext1') === compareLocale('a.ext1', 'a.Ext1'), 'same names with equal and extensions except for case are sorted in full filename locale order');
|
||||
|
||||
//
|
||||
// Comparisons with different results than compareFileNamesDefault
|
||||
//
|
||||
|
||||
// name-only comparisons
|
||||
assert(compareFileNamesUpper('Z', 'a') < 0, 'Z comes before a');
|
||||
assert(compareFileNamesUpper('a', 'A') > 0, 'the same letter sorts uppercase first');
|
||||
assert(compareFileNamesUpper('â', 'Â') > 0, 'the same accented letter sorts uppercase first');
|
||||
assert.deepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesUpper), ['Art', 'Artichoke', 'art', 'artichoke'], 'names with the same root and different cases sort uppercase first');
|
||||
assert.deepStrictEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNamesUpper), ['Email', 'Émail', 'email', 'émail'], 'the same base characters with different case or accents sort uppercase first');
|
||||
|
||||
// numeric comparisons
|
||||
assert(compareFileNamesUpper('art01', 'Art01') > 0, 'a numerically equivalent name of a different case compares uppercase first');
|
||||
assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileNamesUpper), ['A2.txt', 'A100.txt', 'a10.txt', 'a20.txt'], 'filenames with number and case differences group by case then compare by number');
|
||||
|
||||
});
|
||||
|
||||
test('compareFileExtensionsUpper', () => {
|
||||
|
||||
//
|
||||
// Comparisons with the same result as compareFileExtensionsDefault
|
||||
//
|
||||
|
||||
// name-only comparisons
|
||||
assert(compareFileExtensionsUpper(null, null) === 0, 'null should be equal');
|
||||
assert(compareFileExtensionsUpper(null, 'abc') < 0, 'null should come before real files without extensions');
|
||||
assert(compareFileExtensionsUpper('', '') === 0, 'empty should be equal');
|
||||
assert(compareFileExtensionsUpper('abc', 'abc') === 0, 'equal names should be equal');
|
||||
assert(compareFileExtensionsUpper('z', 'A') > 0, 'z comes after A');
|
||||
|
||||
// name plus extension comparisons
|
||||
assert(compareFileExtensionsUpper('file.ext', 'file.ext') === 0, 'equal full filenames should be equal');
|
||||
assert(compareFileExtensionsUpper('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared');
|
||||
assert(compareFileExtensionsUpper('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions');
|
||||
assert(compareFileExtensionsUpper('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first');
|
||||
assert(compareFileExtensionsUpper('a.md', 'b.MD') < 0, 'when extensions are the same except for case, the files sort by name');
|
||||
assert(compareFileExtensionsUpper('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale');
|
||||
assert(compareFileExtensionsUpper('aggregate.go', 'aggregate_repo.go') > 0, 'when extensions are equal, compares the full filename');
|
||||
|
||||
// dotfile comparisons
|
||||
assert(compareFileExtensionsUpper('.abc', '.abc') === 0, 'equal dotfiles should be equal');
|
||||
assert(compareFileExtensionsUpper('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case');
|
||||
assert(compareFileExtensionsUpper('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots');
|
||||
assert(compareFileExtensionsUpper('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first');
|
||||
|
||||
// dotfile vs non-dotfile comparisons
|
||||
assert(compareFileExtensionsUpper(null, '.abc') < 0, 'null should come before dotfiles');
|
||||
assert(compareFileExtensionsUpper('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions');
|
||||
assert(compareFileExtensionsUpper('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files');
|
||||
assert(compareFileExtensionsUpper('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions');
|
||||
assert(compareFileExtensionsUpper('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files');
|
||||
|
||||
// numeric comparisons
|
||||
assert(compareFileExtensionsUpper('1', '1') === 0, 'numerically equal full names should be equal');
|
||||
assert(compareFileExtensionsUpper('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal');
|
||||
assert(compareFileExtensionsUpper('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order');
|
||||
assert(compareFileExtensionsUpper('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order');
|
||||
assert(compareFileExtensionsUpper('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically');
|
||||
assert(compareFileExtensionsUpper('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number');
|
||||
assert(compareFileExtensionsUpper('abc2.txt2', 'abc1.txt10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order');
|
||||
assert(compareFileExtensionsUpper('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal');
|
||||
assert(compareFileExtensionsUpper('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order');
|
||||
assert(compareFileExtensionsUpper('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long');
|
||||
assert(compareFileExtensionsUpper('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, full filenames should be compared');
|
||||
assert(compareFileExtensionsUpper('abc.txt01', 'abc.txt1') > 0, 'extensions with equal numbers should be in shortest-first order');
|
||||
assert(compareFileExtensionsUpper('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first');
|
||||
assert(compareFileExtensionsUpper('txt.abc01', 'txt.abc1') > 0, 'extensions with equivalent numbers sort shortest extension first');
|
||||
assert(compareFileExtensionsUpper('a.ext1', 'b.Ext1') < 0, 'different names and extensions that are equal except for case are sorted in full filename order');
|
||||
assert(compareFileExtensionsUpper('a.ext1', 'a.Ext1') === compareLocale('a.ext1', 'b.Ext1'), 'same names and extensions that are equal except for case are sorted in full filename locale order');
|
||||
|
||||
//
|
||||
// Comparisons with different results than compareFileExtensionsDefault
|
||||
//
|
||||
|
||||
// name-only comparisons
|
||||
assert(compareFileExtensionsUpper('Z', 'a') < 0, 'Z comes before a');
|
||||
assert(compareFileExtensionsUpper('a', 'A') > 0, 'the same letter sorts uppercase first');
|
||||
assert(compareFileExtensionsUpper('â', 'Â') > 0, 'the same accented letter sorts uppercase first');
|
||||
assert.deepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsUpper), ['Art', 'Artichoke', 'art', 'artichoke'], 'names with the same root and different cases sort uppercase names first');
|
||||
assert.deepStrictEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensionsUpper), ['Email', 'Émail', 'email', 'émail'], 'the same base characters with different case or accents sort uppercase names first');
|
||||
|
||||
// name plus extension comparisons
|
||||
assert(compareFileExtensionsUpper('a.md', 'A.md') > 0, 'case differences in names sort uppercase first');
|
||||
assert(compareFileExtensionsUpper('art01', 'Art01') > 0, 'a numerically equivalent word of a different case sorts uppercase first');
|
||||
assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileExtensionsUpper), ['A2.txt', 'A100.txt', 'a10.txt', 'a20.txt',], 'filenames with number and case differences group by case then sort by number');
|
||||
|
||||
});
|
||||
|
||||
test('compareFileNamesLower', () => {
|
||||
|
||||
//
|
||||
// Comparisons with the same results as compareFileNamesDefault
|
||||
//
|
||||
|
||||
// name-only comparisons
|
||||
assert(compareFileNamesLower(null, null) === 0, 'null should be equal');
|
||||
assert(compareFileNamesLower(null, 'abc') < 0, 'null should be come before real values');
|
||||
assert(compareFileNamesLower('', '') === 0, 'empty should be equal');
|
||||
assert(compareFileNamesLower('abc', 'abc') === 0, 'equal names should be equal');
|
||||
assert(compareFileNamesLower('Z', 'a') > 0, 'Z comes after a');
|
||||
|
||||
// name plus extension comparisons
|
||||
assert(compareFileNamesLower('file.ext', 'file.ext') === 0, 'equal full names should be equal');
|
||||
assert(compareFileNamesLower('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared');
|
||||
assert(compareFileNamesLower('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions');
|
||||
assert(compareFileNamesLower('bbb.aaa', 'aaa.bbb') > 0, 'files should be compared by names even if extensions compare differently');
|
||||
assert(compareFileNamesLower('aggregate.go', 'aggregate_repo.go') > 0, 'compares full filenames');
|
||||
|
||||
// dotfile comparisons
|
||||
assert(compareFileNamesLower('.abc', '.abc') === 0, 'equal dotfile names should be equal');
|
||||
assert(compareFileNamesLower('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly');
|
||||
assert(compareFileNamesLower('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots');
|
||||
assert(compareFileNamesLower('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first');
|
||||
assert(compareFileNamesLower('.aaa_env', '.aaa.env') < 0, 'an underscore in a dotfile name will sort before a dot');
|
||||
|
||||
// dotfile vs non-dotfile comparisons
|
||||
assert(compareFileNamesLower(null, '.abc') < 0, 'null should come before dotfiles');
|
||||
assert(compareFileNamesLower('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions');
|
||||
assert(compareFileNamesLower('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions');
|
||||
assert(compareFileNamesLower('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files');
|
||||
assert(compareFileNamesLower('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files');
|
||||
|
||||
// numeric comparisons
|
||||
assert(compareFileNamesLower('1', '1') === 0, 'numerically equal full names should be equal');
|
||||
assert(compareFileNamesLower('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal');
|
||||
assert(compareFileNamesLower('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order');
|
||||
assert(compareFileNamesLower('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long');
|
||||
assert(compareFileNamesLower('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically');
|
||||
assert(compareFileNamesLower('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number');
|
||||
assert(compareFileNamesLower('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first');
|
||||
assert(compareFileNamesLower('abc.txt1', 'abc.txt01') < 0, 'same name plus extensions with equal numbers sort shortest number first');
|
||||
assert(compareFileNamesLower('a.ext1', 'b.Ext1') < 0, 'different names and extensions that are equal except for case are sorted in full filename order');
|
||||
assert(compareFileNamesLower('a.ext1', 'a.Ext1') === compareLocale('a.ext1', 'b.Ext1'), 'same names and extensions that are equal except for case are sorted in full filename locale order');
|
||||
|
||||
//
|
||||
// Comparisons with different results than compareFileNamesDefault
|
||||
//
|
||||
|
||||
// name-only comparisons
|
||||
assert(compareFileNamesLower('z', 'A') < 0, 'z comes before A');
|
||||
assert(compareFileNamesLower('a', 'A') < 0, 'the same letter sorts lowercase first');
|
||||
assert(compareFileNamesLower('â', 'Â') < 0, 'the same accented letter sorts lowercase first');
|
||||
assert.deepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesLower), ['art', 'artichoke', 'Art', 'Artichoke'], 'names with the same root and different cases sort lowercase first');
|
||||
assert.deepStrictEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNamesLower), ['email', 'émail', 'Email', 'Émail'], 'the same base characters with different case or accents sort lowercase first');
|
||||
|
||||
// numeric comparisons
|
||||
assert(compareFileNamesLower('art01', 'Art01') < 0, 'a numerically equivalent name of a different case compares lowercase first');
|
||||
assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileNamesLower), ['a10.txt', 'a20.txt', 'A2.txt', 'A100.txt'], 'filenames with number and case differences group by case then compare by number');
|
||||
|
||||
});
|
||||
|
||||
test('compareFileExtensionsLower', () => {
|
||||
|
||||
//
|
||||
// Comparisons with the same result as compareFileExtensionsDefault
|
||||
//
|
||||
|
||||
// name-only comparisons
|
||||
assert(compareFileExtensionsLower(null, null) === 0, 'null should be equal');
|
||||
assert(compareFileExtensionsLower(null, 'abc') < 0, 'null should come before real files without extensions');
|
||||
assert(compareFileExtensionsLower('', '') === 0, 'empty should be equal');
|
||||
assert(compareFileExtensionsLower('abc', 'abc') === 0, 'equal names should be equal');
|
||||
assert(compareFileExtensionsLower('Z', 'a') > 0, 'Z comes after a');
|
||||
|
||||
// name plus extension comparisons
|
||||
assert(compareFileExtensionsLower('file.ext', 'file.ext') === 0, 'equal full filenames should be equal');
|
||||
assert(compareFileExtensionsLower('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared');
|
||||
assert(compareFileExtensionsLower('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions');
|
||||
assert(compareFileExtensionsLower('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first');
|
||||
assert(compareFileExtensionsLower('a.md', 'b.MD') < 0, 'when extensions are the same except for case, the files sort by name');
|
||||
assert(compareFileExtensionsLower('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale');
|
||||
|
||||
// dotfile comparisons
|
||||
assert(compareFileExtensionsLower('.abc', '.abc') === 0, 'equal dotfiles should be equal');
|
||||
assert(compareFileExtensionsLower('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case');
|
||||
assert(compareFileExtensionsLower('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots');
|
||||
assert(compareFileExtensionsLower('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first');
|
||||
|
||||
// dotfile vs non-dotfile comparisons
|
||||
assert(compareFileExtensionsLower(null, '.abc') < 0, 'null should come before dotfiles');
|
||||
assert(compareFileExtensionsLower('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions');
|
||||
assert(compareFileExtensionsLower('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files');
|
||||
assert(compareFileExtensionsLower('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions');
|
||||
assert(compareFileExtensionsLower('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files');
|
||||
|
||||
// numeric comparisons
|
||||
assert(compareFileExtensionsLower('1', '1') === 0, 'numerically equal full names should be equal');
|
||||
assert(compareFileExtensionsLower('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal');
|
||||
assert(compareFileExtensionsLower('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order');
|
||||
assert(compareFileExtensionsLower('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order');
|
||||
assert(compareFileExtensionsLower('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically');
|
||||
assert(compareFileExtensionsLower('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number');
|
||||
assert(compareFileExtensionsLower('abc2.txt2', 'abc1.txt10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order');
|
||||
assert(compareFileExtensionsLower('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal');
|
||||
assert(compareFileExtensionsLower('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order');
|
||||
assert(compareFileExtensionsLower('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long');
|
||||
assert(compareFileExtensionsLower('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, full filenames should be compared');
|
||||
assert(compareFileExtensionsLower('abc.txt01', 'abc.txt1') > 0, 'extensions with equal numbers should be in shortest-first order');
|
||||
assert(compareFileExtensionsLower('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first');
|
||||
assert(compareFileExtensionsLower('txt.abc01', 'txt.abc1') > 0, 'extensions with equivalent numbers sort shortest extension first');
|
||||
assert(compareFileExtensionsLower('a.ext1', 'b.Ext1') < 0, 'if extensions with numbers are equal except for case, full filenames should be compared');
|
||||
assert(compareFileExtensionsLower('a.ext1', 'a.Ext1') === compareLocale('a.ext1', 'a.Ext1'), 'if extensions with numbers are equal except for case, filenames are sorted in locale order');
|
||||
|
||||
//
|
||||
// Comparisons with different results than compareFileExtensionsDefault
|
||||
//
|
||||
|
||||
// name-only comparisons
|
||||
assert(compareFileExtensionsLower('z', 'A') < 0, 'z comes before A');
|
||||
assert(compareFileExtensionsLower('a', 'A') < 0, 'the same letter sorts lowercase first');
|
||||
assert(compareFileExtensionsLower('â', 'Â') < 0, 'the same accented letter sorts lowercase first');
|
||||
assert.deepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsLower), ['art', 'artichoke', 'Art', 'Artichoke'], 'names with the same root and different cases sort lowercase names first');
|
||||
assert.deepStrictEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensionsLower), ['email', 'émail', 'Email', 'Émail'], 'the same base characters with different case or accents sort lowercase names first');
|
||||
|
||||
// name plus extension comparisons
|
||||
assert(compareFileExtensionsLower('a.md', 'A.md') < 0, 'case differences in names sort lowercase first');
|
||||
assert(compareFileExtensionsLower('art01', 'Art01') < 0, 'a numerically equivalent word of a different case sorts lowercase first');
|
||||
assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileExtensionsLower), ['a10.txt', 'a20.txt', 'A2.txt', 'A100.txt'], 'filenames with number and case differences group by case then sort by number');
|
||||
assert(compareFileExtensionsLower('aggregate.go', 'aggregate_repo.go') > 0, 'when extensions are equal, compares full filenames');
|
||||
|
||||
});
|
||||
|
||||
test('compareFileNamesUnicode', () => {
|
||||
|
||||
//
|
||||
// Comparisons with the same results as compareFileNamesDefault
|
||||
//
|
||||
|
||||
// name-only comparisons
|
||||
assert(compareFileNamesUnicode(null, null) === 0, 'null should be equal');
|
||||
assert(compareFileNamesUnicode(null, 'abc') < 0, 'null should be come before real values');
|
||||
assert(compareFileNamesUnicode('', '') === 0, 'empty should be equal');
|
||||
assert(compareFileNamesUnicode('abc', 'abc') === 0, 'equal names should be equal');
|
||||
assert(compareFileNamesUnicode('z', 'A') > 0, 'z comes after A');
|
||||
|
||||
// name plus extension comparisons
|
||||
assert(compareFileNamesUnicode('file.ext', 'file.ext') === 0, 'equal full names should be equal');
|
||||
assert(compareFileNamesUnicode('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared');
|
||||
assert(compareFileNamesUnicode('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions');
|
||||
assert(compareFileNamesUnicode('bbb.aaa', 'aaa.bbb') > 0, 'files should be compared by names even if extensions compare differently');
|
||||
|
||||
// dotfile comparisons
|
||||
assert(compareFileNamesUnicode('.abc', '.abc') === 0, 'equal dotfile names should be equal');
|
||||
assert(compareFileNamesUnicode('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly');
|
||||
assert(compareFileNamesUnicode('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots');
|
||||
assert(compareFileNamesUnicode('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first');
|
||||
|
||||
// dotfile vs non-dotfile comparisons
|
||||
assert(compareFileNamesUnicode(null, '.abc') < 0, 'null should come before dotfiles');
|
||||
assert(compareFileNamesUnicode('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions');
|
||||
assert(compareFileNamesUnicode('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions');
|
||||
assert(compareFileNamesUnicode('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files');
|
||||
assert(compareFileNamesUnicode('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files');
|
||||
|
||||
// numeric comparisons
|
||||
assert(compareFileNamesUnicode('1', '1') === 0, 'numerically equal full names should be equal');
|
||||
assert(compareFileNamesUnicode('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal');
|
||||
assert(compareFileNamesUnicode('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order');
|
||||
assert(compareFileNamesUnicode('a.ext1', 'b.Ext1') < 0, 'if names are different and extensions with numbers are equal except for case, filenames are sorted by unicode full filename');
|
||||
assert(compareFileNamesUnicode('a.ext1', 'a.Ext1') > 0, 'if names are equal and extensions with numbers are equal except for case, filenames are sorted by unicode full filename');
|
||||
|
||||
//
|
||||
// Comparisons with different results than compareFileNamesDefault
|
||||
//
|
||||
|
||||
// name-only comparisons
|
||||
assert(compareFileNamesUnicode('Z', 'a') < 0, 'Z comes before a');
|
||||
assert(compareFileNamesUnicode('a', 'A') > 0, 'the same letter sorts uppercase first');
|
||||
assert(compareFileNamesUnicode('â', 'Â') > 0, 'the same accented letter sorts uppercase first');
|
||||
assert.deepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesUnicode), ['Art', 'Artichoke', 'art', 'artichoke'], 'names with the same root and different cases sort uppercase first');
|
||||
assert.deepStrictEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNamesUnicode), ['Email', 'email', 'Émail', 'émail'], 'the same base characters with different case or accents sort in unicode order');
|
||||
|
||||
// name plus extension comparisons
|
||||
assert(compareFileNamesUnicode('aggregate.go', 'aggregate_repo.go') < 0, 'compares the whole name in unicode order, but dot comes before underscore');
|
||||
|
||||
// dotfile comparisons
|
||||
assert(compareFileNamesUnicode('.aaa_env', '.aaa.env') > 0, 'an underscore in a dotfile name will sort after a dot');
|
||||
|
||||
// numeric comparisons
|
||||
assert(compareFileNamesUnicode('abc2.txt', 'abc10.txt') > 0, 'filenames with numbers should be in unicode order even when they are multiple digits long');
|
||||
assert(compareFileNamesUnicode('abc02.txt', 'abc010.txt') > 0, 'filenames with numbers that have leading zeros sort in unicode order');
|
||||
assert(compareFileNamesUnicode('abc1.10.txt', 'abc1.2.txt') < 0, 'numbers with dots between them are sorted in unicode order');
|
||||
assert(compareFileNamesUnicode('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort in unicode order');
|
||||
assert(compareFileNamesUnicode('abc.txt1', 'abc.txt01') > 0, 'same name plus extensions with equal numbers sort in unicode order');
|
||||
assert(compareFileNamesUnicode('art01', 'Art01') > 0, 'a numerically equivalent name of a different case compares uppercase first');
|
||||
assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileNamesUnicode), ['A100.txt', 'A2.txt', 'a10.txt', 'a20.txt'], 'filenames with number and case differences sort in unicode order');
|
||||
|
||||
});
|
||||
|
||||
test('compareFileExtensionsUnicode', () => {
|
||||
|
||||
//
|
||||
// Comparisons with the same result as compareFileExtensionsDefault
|
||||
//
|
||||
|
||||
// name-only comparisons
|
||||
assert(compareFileExtensionsUnicode(null, null) === 0, 'null should be equal');
|
||||
assert(compareFileExtensionsUnicode(null, 'abc') < 0, 'null should come before real files without extensions');
|
||||
assert(compareFileExtensionsUnicode('', '') === 0, 'empty should be equal');
|
||||
assert(compareFileExtensionsUnicode('abc', 'abc') === 0, 'equal names should be equal');
|
||||
assert(compareFileExtensionsUnicode('z', 'A') > 0, 'z comes after A');
|
||||
|
||||
// name plus extension comparisons
|
||||
assert(compareFileExtensionsUnicode('file.ext', 'file.ext') === 0, 'equal full filenames should be equal');
|
||||
assert(compareFileExtensionsUnicode('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared');
|
||||
assert(compareFileExtensionsUnicode('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions');
|
||||
assert(compareFileExtensionsUnicode('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first');
|
||||
assert(compareFileExtensionsUnicode('a.md', 'b.MD') < 0, 'when extensions are the same except for case, the files sort by name');
|
||||
assert(compareFileExtensionsUnicode('a.MD', 'a.md') < 0, 'case differences in extensions sort in unicode order');
|
||||
|
||||
// dotfile comparisons
|
||||
assert(compareFileExtensionsUnicode('.abc', '.abc') === 0, 'equal dotfiles should be equal');
|
||||
assert(compareFileExtensionsUnicode('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case');
|
||||
assert(compareFileExtensionsUnicode('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots');
|
||||
assert(compareFileExtensionsUnicode('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first');
|
||||
|
||||
// dotfile vs non-dotfile comparisons
|
||||
assert(compareFileExtensionsUnicode(null, '.abc') < 0, 'null should come before dotfiles');
|
||||
assert(compareFileExtensionsUnicode('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions');
|
||||
assert(compareFileExtensionsUnicode('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files');
|
||||
assert(compareFileExtensionsUnicode('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions');
|
||||
assert(compareFileExtensionsUnicode('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files');
|
||||
|
||||
// numeric comparisons
|
||||
assert(compareFileExtensionsUnicode('1', '1') === 0, 'numerically equal full names should be equal');
|
||||
assert(compareFileExtensionsUnicode('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal');
|
||||
assert(compareFileExtensionsUnicode('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order');
|
||||
assert(compareFileExtensionsUnicode('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal');
|
||||
assert(compareFileExtensionsUnicode('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order');
|
||||
assert(compareFileExtensionsUnicode('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, full filenames should be compared');
|
||||
|
||||
//
|
||||
// Comparisons with different results than compareFileExtensionsDefault
|
||||
//
|
||||
|
||||
// name-only comparisons
|
||||
assert(compareFileExtensionsUnicode('Z', 'a') < 0, 'Z comes before a');
|
||||
assert(compareFileExtensionsUnicode('a', 'A') > 0, 'the same letter sorts uppercase first');
|
||||
assert(compareFileExtensionsUnicode('â', 'Â') > 0, 'the same accented letter sorts uppercase first');
|
||||
assert.deepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsUnicode), ['Art', 'Artichoke', 'art', 'artichoke'], 'names with the same root and different cases sort uppercase names first');
|
||||
assert.deepStrictEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensionsUnicode), ['Email', 'email', 'Émail', 'émail'], 'the same base characters with different case or accents sort in unicode order');
|
||||
|
||||
// name plus extension comparisons
|
||||
assert(compareFileExtensionsUnicode('a.MD', 'a.md') < 0, 'case differences in extensions sort by uppercase extension first');
|
||||
assert(compareFileExtensionsUnicode('a.md', 'A.md') > 0, 'case differences in names sort uppercase first');
|
||||
assert(compareFileExtensionsUnicode('art01', 'Art01') > 0, 'a numerically equivalent name of a different case sorts uppercase first');
|
||||
assert.deepStrictEqual(['a10.txt', 'A2.txt', 'A100.txt', 'a20.txt'].sort(compareFileExtensionsUnicode), ['A100.txt', 'A2.txt', 'a10.txt', 'a20.txt'], 'filenames with number and case differences sort in unicode order');
|
||||
assert(compareFileExtensionsUnicode('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, compares full filenames in unicode order');
|
||||
|
||||
// numeric comparisons
|
||||
assert(compareFileExtensionsUnicode('abc2.txt', 'abc10.txt') > 0, 'filenames with numbers should be in unicode order');
|
||||
assert(compareFileExtensionsUnicode('abc02.txt', 'abc010.txt') > 0, 'filenames with numbers that have leading zeros sort in unicode order');
|
||||
assert(compareFileExtensionsUnicode('abc1.10.txt', 'abc1.2.txt') < 0, 'numbers with dots between them sort in unicode order');
|
||||
assert(compareFileExtensionsUnicode('abc2.txt2', 'abc1.txt10') > 0, 'extensions with numbers should be in unicode order');
|
||||
assert(compareFileExtensionsUnicode('txt.abc2', 'txt.abc10') > 0, 'extensions with numbers should be in unicode order even when they are multiple digits long');
|
||||
assert(compareFileExtensionsUnicode('abc.txt01', 'abc.txt1') < 0, 'extensions with equal numbers should be in unicode order');
|
||||
assert(compareFileExtensionsUnicode('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort in unicode order');
|
||||
assert(compareFileExtensionsUnicode('txt.abc01', 'txt.abc1') < 0, 'extensions with equivalent numbers sort in unicode order');
|
||||
assert(compareFileExtensionsUnicode('a.ext1', 'b.Ext1') < 0, 'if extensions with numbers are equal except for case, unicode full filenames should be compared');
|
||||
assert(compareFileExtensionsUnicode('a.ext1', 'a.Ext1') > 0, 'if extensions with numbers are equal except for case, unicode full filenames should be compared');
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -296,45 +296,19 @@ suite('Arrays', () => {
|
||||
assert.strictEqual(array.length, 0);
|
||||
});
|
||||
|
||||
test('splice', function () {
|
||||
// negative start index, absolute value greater than the length
|
||||
let array = [1, 2, 3, 4, 5];
|
||||
arrays.splice(array, -6, 3, [6, 7]);
|
||||
assert.strictEqual(array.length, 4);
|
||||
assert.strictEqual(array[0], 6);
|
||||
assert.strictEqual(array[1], 7);
|
||||
assert.strictEqual(array[2], 4);
|
||||
assert.strictEqual(array[3], 5);
|
||||
test('minIndex', () => {
|
||||
const array = ['a', 'b', 'c'];
|
||||
assert.strictEqual(arrays.minIndex(array, value => array.indexOf(value)), 0);
|
||||
assert.strictEqual(arrays.minIndex(array, value => -array.indexOf(value)), 2);
|
||||
assert.strictEqual(arrays.minIndex(array, _value => 0), 0);
|
||||
assert.strictEqual(arrays.minIndex(array, value => value === 'b' ? 0 : 5), 1);
|
||||
});
|
||||
|
||||
// negative start index, absolute value less than the length
|
||||
array = [1, 2, 3, 4, 5];
|
||||
arrays.splice(array, -3, 3, [6, 7]);
|
||||
assert.strictEqual(array.length, 4);
|
||||
assert.strictEqual(array[0], 1);
|
||||
assert.strictEqual(array[1], 2);
|
||||
assert.strictEqual(array[2], 6);
|
||||
assert.strictEqual(array[3], 7);
|
||||
|
||||
// Start index less than the length
|
||||
array = [1, 2, 3, 4, 5];
|
||||
arrays.splice(array, 3, 3, [6, 7]);
|
||||
assert.strictEqual(array.length, 5);
|
||||
assert.strictEqual(array[0], 1);
|
||||
assert.strictEqual(array[1], 2);
|
||||
assert.strictEqual(array[2], 3);
|
||||
assert.strictEqual(array[3], 6);
|
||||
assert.strictEqual(array[4], 7);
|
||||
|
||||
// Start index greater than the length
|
||||
array = [1, 2, 3, 4, 5];
|
||||
arrays.splice(array, 6, 3, [6, 7]);
|
||||
assert.strictEqual(array.length, 7);
|
||||
assert.strictEqual(array[0], 1);
|
||||
assert.strictEqual(array[1], 2);
|
||||
assert.strictEqual(array[2], 3);
|
||||
assert.strictEqual(array[3], 4);
|
||||
assert.strictEqual(array[4], 5);
|
||||
assert.strictEqual(array[5], 6);
|
||||
assert.strictEqual(array[6], 7);
|
||||
test('maxIndex', () => {
|
||||
const array = ['a', 'b', 'c'];
|
||||
assert.strictEqual(arrays.maxIndex(array, value => array.indexOf(value)), 2);
|
||||
assert.strictEqual(arrays.maxIndex(array, value => -array.indexOf(value)), 0);
|
||||
assert.strictEqual(arrays.maxIndex(array, _value => 0), 0);
|
||||
assert.strictEqual(arrays.maxIndex(array, value => value === 'b' ? 5 : 0), 1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -561,7 +561,7 @@ suite('Async', () => {
|
||||
});
|
||||
|
||||
test('TaskSequentializer - pending basics', async function () {
|
||||
const sequentializer: any = new async.TaskSequentializer();
|
||||
const sequentializer: any = new async.TaskSequentializer(); // {{SQL CARBON EDIT}} Add any type
|
||||
|
||||
assert.ok(!sequentializer.hasPending());
|
||||
assert.ok(!sequentializer.hasPending(2323));
|
||||
|
||||
@@ -4,75 +4,25 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IMatch } from 'vs/base/common/filters';
|
||||
import { matchesFuzzyCodiconAware, parseCodicons, IParsedCodicons } from 'vs/base/common/codicon';
|
||||
import { stripCodicons } from 'vs/base/common/codicons';
|
||||
|
||||
export interface ICodiconFilter {
|
||||
// Returns null if word doesn't match.
|
||||
(query: string, target: IParsedCodicons): IMatch[] | null;
|
||||
}
|
||||
|
||||
function filterOk(filter: ICodiconFilter, word: string, target: IParsedCodicons, highlights?: { start: number; end: number; }[]) {
|
||||
let r = filter(word, target);
|
||||
assert(r);
|
||||
if (highlights) {
|
||||
assert.deepEqual(r, highlights);
|
||||
}
|
||||
}
|
||||
import { getCodiconAriaLabel } from 'vs/base/common/codicons';
|
||||
|
||||
suite('Codicon', () => {
|
||||
test('matchesFuzzzyCodiconAware', () => {
|
||||
|
||||
// Camel Case
|
||||
|
||||
filterOk(matchesFuzzyCodiconAware, 'ccr', parseCodicons('$(codicon)CamelCaseRocks$(codicon)'), [
|
||||
{ start: 10, end: 11 },
|
||||
{ start: 15, end: 16 },
|
||||
{ start: 19, end: 20 }
|
||||
test('Can get proper aria labels', () => {
|
||||
// note, the spaces in the results are important
|
||||
const testCases = new Map<string, string>([
|
||||
['', ''],
|
||||
['asdf', 'asdf'],
|
||||
['asdf$(squirrel)asdf', 'asdf squirrel asdf'],
|
||||
['asdf $(squirrel) asdf', 'asdf squirrel asdf'],
|
||||
['$(rocket)asdf', 'rocket asdf'],
|
||||
['$(rocket) asdf', 'rocket asdf'],
|
||||
['$(rocket)$(rocket)$(rocket)asdf', 'rocket rocket rocket asdf'],
|
||||
['$(rocket) asdf $(rocket)', 'rocket asdf rocket'],
|
||||
['$(rocket)asdf$(rocket)', 'rocket asdf rocket'],
|
||||
]);
|
||||
|
||||
filterOk(matchesFuzzyCodiconAware, 'ccr', parseCodicons('$(codicon) CamelCaseRocks $(codicon)'), [
|
||||
{ start: 11, end: 12 },
|
||||
{ start: 16, end: 17 },
|
||||
{ start: 20, end: 21 }
|
||||
]);
|
||||
|
||||
filterOk(matchesFuzzyCodiconAware, 'iut', parseCodicons('$(codicon) Indent $(octico) Using $(octic) Tpaces'), [
|
||||
{ start: 11, end: 12 },
|
||||
{ start: 28, end: 29 },
|
||||
{ start: 43, end: 44 },
|
||||
]);
|
||||
|
||||
// Prefix
|
||||
|
||||
filterOk(matchesFuzzyCodiconAware, 'using', parseCodicons('$(codicon) Indent Using Spaces'), [
|
||||
{ start: 18, end: 23 },
|
||||
]);
|
||||
|
||||
// Broken Codicon
|
||||
|
||||
filterOk(matchesFuzzyCodiconAware, 'codicon', parseCodicons('This $(codicon Indent Using Spaces'), [
|
||||
{ start: 7, end: 14 },
|
||||
]);
|
||||
|
||||
filterOk(matchesFuzzyCodiconAware, 'indent', parseCodicons('This $codicon Indent Using Spaces'), [
|
||||
{ start: 14, end: 20 },
|
||||
]);
|
||||
|
||||
// Testing #59343
|
||||
filterOk(matchesFuzzyCodiconAware, 'unt', parseCodicons('$(primitive-dot) $(file-text) Untitled-1'), [
|
||||
{ start: 30, end: 33 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Codicons', () => {
|
||||
|
||||
test('stripCodicons', () => {
|
||||
assert.equal(stripCodicons('Hello World'), 'Hello World');
|
||||
assert.equal(stripCodicons('$(Hello World'), '$(Hello World');
|
||||
assert.equal(stripCodicons('$(Hello) World'), ' World');
|
||||
assert.equal(stripCodicons('$(Hello) W$(oi)rld'), ' Wrld');
|
||||
for (const [input, expected] of testCases) {
|
||||
assert.strictEqual(getCodiconAriaLabel(input), expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
|
||||
@@ -53,26 +53,4 @@ suite('Collections', () => {
|
||||
assert.strictEqual(grouped[group2].length, 1);
|
||||
assert.strictEqual(grouped[group2][0].value, value3);
|
||||
});
|
||||
|
||||
test('groupByNumber', () => {
|
||||
|
||||
const group1 = 1, group2 = 2;
|
||||
const value1 = 'a', value2 = 'b', value3 = 'c';
|
||||
let source = [
|
||||
{ key: group1, value: value1 },
|
||||
{ key: group1, value: value2 },
|
||||
{ key: group2, value: value3 },
|
||||
];
|
||||
|
||||
let grouped = collections.groupByNumber(source, x => x.key);
|
||||
|
||||
// Group 1
|
||||
assert.strictEqual(grouped.get(group1)!.length, 2);
|
||||
assert.strictEqual(grouped.get(group1)![0].value, value1);
|
||||
assert.strictEqual(grouped.get(group1)![1].value, value2);
|
||||
|
||||
// Group 2
|
||||
assert.strictEqual(grouped.get(group2)!.length, 1);
|
||||
assert.strictEqual(grouped.get(group2)![0].value, value3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import * as sinon from 'sinon';
|
||||
import * as assert from 'assert';
|
||||
import { memoize, createMemoizer, throttle } from 'vs/base/common/decorators';
|
||||
import { memoize, throttle } from 'vs/base/common/decorators';
|
||||
|
||||
suite('Decorators', () => {
|
||||
test('memoize should memoize methods', () => {
|
||||
@@ -131,28 +131,6 @@ suite('Decorators', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('memoize clear', () => {
|
||||
const memoizer = createMemoizer();
|
||||
let counter = 0;
|
||||
class Foo {
|
||||
@memoizer
|
||||
get answer() {
|
||||
return ++counter;
|
||||
}
|
||||
}
|
||||
|
||||
const foo = new Foo();
|
||||
assert.strictEqual(foo.answer, 1);
|
||||
assert.strictEqual(foo.answer, 1);
|
||||
memoizer.clear();
|
||||
assert.strictEqual(foo.answer, 2);
|
||||
assert.strictEqual(foo.answer, 2);
|
||||
memoizer.clear();
|
||||
assert.strictEqual(foo.answer, 3);
|
||||
assert.strictEqual(foo.answer, 3);
|
||||
assert.strictEqual(foo.answer, 3);
|
||||
});
|
||||
|
||||
test('throttle', () => {
|
||||
const spy = sinon.spy();
|
||||
const clock = sinon.useFakeTimers();
|
||||
@@ -183,7 +161,7 @@ suite('Decorators', () => {
|
||||
|
||||
clock.tick(200);
|
||||
assert.deepStrictEqual(spy.args, [[1], [5]]);
|
||||
spy.reset();
|
||||
spy.resetHistory();
|
||||
|
||||
t.report(4);
|
||||
t.report(5);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import { Event, Emitter, EventBufferer, EventMultiplexer, PauseableEmitter } from 'vs/base/common/event';
|
||||
import { Event, Emitter, EventBufferer, EventMultiplexer, PauseableEmitter, Relay } from 'vs/base/common/event';
|
||||
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { errorHandler, setUnexpectedErrorHandler } from 'vs/base/common/errors';
|
||||
import { AsyncEmitter, IWaitUntil, timeout } from 'vs/base/common/async';
|
||||
@@ -894,4 +894,70 @@ suite('Event utils', () => {
|
||||
listener.dispose();
|
||||
});
|
||||
|
||||
test('dispose is reentrant', () => {
|
||||
const emitter = new Emitter<number>({
|
||||
onLastListenerRemove: () => {
|
||||
emitter.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
const listener = emitter.event(() => undefined);
|
||||
listener.dispose(); // should not crash
|
||||
});
|
||||
|
||||
suite('Relay', () => {
|
||||
test('should input work', () => {
|
||||
const e1 = new Emitter<number>();
|
||||
const e2 = new Emitter<number>();
|
||||
const relay = new Relay<number>();
|
||||
|
||||
const result: number[] = [];
|
||||
const listener = (num: number) => result.push(num);
|
||||
const subscription = relay.event(listener);
|
||||
|
||||
e1.fire(1);
|
||||
assert.deepStrictEqual(result, []);
|
||||
|
||||
relay.input = e1.event;
|
||||
e1.fire(2);
|
||||
assert.deepStrictEqual(result, [2]);
|
||||
|
||||
relay.input = e2.event;
|
||||
e1.fire(3);
|
||||
e2.fire(4);
|
||||
assert.deepStrictEqual(result, [2, 4]);
|
||||
|
||||
subscription.dispose();
|
||||
e1.fire(5);
|
||||
e2.fire(6);
|
||||
assert.deepStrictEqual(result, [2, 4]);
|
||||
});
|
||||
|
||||
test('should Relay dispose work', () => {
|
||||
const e1 = new Emitter<number>();
|
||||
const e2 = new Emitter<number>();
|
||||
const relay = new Relay<number>();
|
||||
|
||||
const result: number[] = [];
|
||||
const listener = (num: number) => result.push(num);
|
||||
relay.event(listener);
|
||||
|
||||
e1.fire(1);
|
||||
assert.deepStrictEqual(result, []);
|
||||
|
||||
relay.input = e1.event;
|
||||
e1.fire(2);
|
||||
assert.deepStrictEqual(result, [2]);
|
||||
|
||||
relay.input = e2.event;
|
||||
e1.fire(3);
|
||||
e2.fire(4);
|
||||
assert.deepStrictEqual(result, [2, 4]);
|
||||
|
||||
relay.dispose();
|
||||
e1.fire(5);
|
||||
e2.fire(6);
|
||||
assert.deepStrictEqual(result, [2, 4]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
export const data: string[];
|
||||
export const data: string[];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user