Merge from vscode 79a1f5a5ca0c6c53db617aa1fa5a2396d2caebe2

This commit is contained in:
ADS Merger
2020-05-31 19:47:51 +00:00
parent 84492049e8
commit 28be33cfea
913 changed files with 28242 additions and 15549 deletions

View File

@@ -34,4 +34,5 @@ export interface IContextMenuDelegate {
actionRunner?: IActionRunner;
autoSelectFirstItem?: boolean;
anchorAlignment?: AnchorAlignment;
anchorAsContainer?: boolean;
}

View File

@@ -16,16 +16,22 @@ import { escape } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { renderCodicons, markdownEscapeEscapedCodicons } from 'vs/base/common/codicons';
import { resolvePath } from 'vs/base/common/resources';
export interface MarkedOptions extends marked.MarkedOptions {
baseUrl?: never;
}
export interface MarkdownRenderOptions extends FormattedTextRenderOptions {
codeBlockRenderer?: (modeId: string, value: string) => Promise<string>;
codeBlockRenderCallback?: () => void;
baseUrl?: URI;
}
/**
* Create html nodes for the given content element.
*/
export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRenderOptions = {}, markedOptions: marked.MarkedOptions = {}): HTMLElement {
export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRenderOptions = {}, markedOptions: MarkedOptions = {}): HTMLElement {
const element = createElement(options);
const _uriMassage = function (part: string): string {
@@ -82,6 +88,9 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
if (href) {
({ href, dimensions } = parseHrefAndDimensions(href));
href = _href(href, true);
if (options.baseUrl) {
href = resolvePath(options.baseUrl, href).toString();
}
attributes.push(`src="${href}"`);
}
if (text) {
@@ -101,6 +110,12 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
text = removeMarkdownEscapes(text);
}
href = _href(href, false);
if (options.baseUrl) {
const hasScheme = /^\w[\w\d+.-]*:/.test(href);
if (!hasScheme) {
href = resolvePath(options.baseUrl, href).toString();
}
}
title = removeMarkdownEscapes(title);
href = removeMarkdownEscapes(href);
if (
@@ -187,6 +202,13 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
}));
}
// Use our own sanitizer so that we can let through only spans.
// Otherwise, we'd be letting all html be rendered.
// If we want to allow markdown permitted tags, then we can delete sanitizer and sanitize.
markedOptions.sanitizer = (html: string): string => {
const match = markdown.isTrusted ? html.match(/^(<span[^<]+>)|(<\/\s*span>)$/) : undefined;
return match ? html : '';
};
markedOptions.sanitize = true;
markedOptions.renderer = renderer;
@@ -202,18 +224,32 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
markedOptions
);
function filter(token: { tag: string, attrs: { readonly [key: string]: string } }): boolean {
if (token.tag === 'span' && markdown.isTrusted) {
if (token.attrs['style'] && Object.keys(token.attrs).length === 1) {
return !!token.attrs['style'].match(/^(color\:#[0-9a-fA-F]+;)?(background-color\:#[0-9a-fA-F]+;)?$/);
}
return false;
}
return true;
}
element.innerHTML = insane(renderedMarkdown, {
allowedSchemes,
// allowedTags should included everything that markdown renders to.
// Since we have our own sanitize function for marked, it's possible we missed some tag so let insane make sure.
// HTML tags that can result from markdown are from reading https://spec.commonmark.org/0.29/
allowedTags: ['ul', 'li', 'p', 'code', 'blockquote', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'em', 'pre', 'table', 'tr', 'td', 'div', 'del', 'a', 'strong', 'br', 'img', 'span'],
allowedAttributes: {
'a': ['href', 'name', 'target', 'data-href'],
'iframe': ['allowfullscreen', 'frameborder', 'src'],
'img': ['src', 'title', 'alt', 'width', 'height'],
'div': ['class', 'data-code'],
'span': ['class'],
'span': ['class', 'style'],
// https://github.com/microsoft/vscode/issues/95937
'th': ['align'],
'td': ['align']
}
},
filter
});
signalInnerHTML!();

View File

@@ -31,6 +31,7 @@ export interface IActionViewItem extends IDisposable {
export interface IBaseActionViewItemOptions {
draggable?: boolean;
isMenu?: boolean;
useEventAsContext?: boolean;
}
export class BaseActionViewItem extends Disposable implements IActionViewItem {
@@ -178,7 +179,7 @@ export class BaseActionViewItem extends Disposable implements IActionViewItem {
onClick(event: DOM.EventLike): void {
DOM.EventHelper.stop(event, true);
const context = types.isUndefinedOrNull(this._context) ? undefined : this._context;
const context = types.isUndefinedOrNull(this._context) ? this.options?.useEventAsContext ? event : undefined : this._context;
this.actionRunner.run(this._action, context);
}
@@ -404,6 +405,7 @@ export interface IActionBarOptions {
ariaLabel?: string;
animated?: boolean;
triggerKeys?: ActionTrigger;
allowContextMenu?: boolean;
}
const defaultOptions: IActionBarOptions = {
@@ -633,9 +635,11 @@ export class ActionBar extends Disposable implements IActionRunner {
actionViewItemElement.setAttribute('role', 'presentation');
// Prevent native context menu on actions
this._register(DOM.addDisposableListener(actionViewItemElement, DOM.EventType.CONTEXT_MENU, (e: DOM.EventLike) => {
DOM.EventHelper.stop(e, true);
}));
if (!this.options.allowContextMenu) {
this._register(DOM.addDisposableListener(actionViewItemElement, DOM.EventType.CONTEXT_MENU, (e: DOM.EventLike) => {
DOM.EventHelper.stop(e, true);
}));
}
let item: IActionViewItem | undefined;

View File

@@ -7,3 +7,12 @@
position: absolute;
z-index: 2500;
}
.context-view.fixed {
all: initial;
font-family: inherit;
font-size: 13px;
position: fixed;
z-index: 2500;
color: inherit;
}

View File

@@ -38,7 +38,7 @@ export interface IDelegate {
}
export interface IContextViewProvider {
showContextView(delegate: IDelegate): void;
showContextView(delegate: IDelegate, container?: HTMLElement): void;
hideContextView(): void;
layout(): void;
}
@@ -104,23 +104,25 @@ export class ContextView extends Disposable {
private container: HTMLElement | null = null;
private view: HTMLElement;
private useFixedPosition: boolean;
private delegate: IDelegate | null = null;
private toDisposeOnClean: IDisposable = Disposable.None;
private toDisposeOnSetContainer: IDisposable = Disposable.None;
constructor(container: HTMLElement) {
constructor(container: HTMLElement, useFixedPosition: boolean) {
super();
this.view = DOM.$('.context-view');
this.useFixedPosition = false;
DOM.hide(this.view);
this.setContainer(container);
this.setContainer(container, useFixedPosition);
this._register(toDisposable(() => this.setContainer(null)));
this._register(toDisposable(() => this.setContainer(null, false)));
}
setContainer(container: HTMLElement | null): void {
setContainer(container: HTMLElement | null, useFixedPosition: boolean): void {
if (this.container) {
this.toDisposeOnSetContainer.dispose();
this.container.removeChild(this.view);
@@ -146,6 +148,8 @@ export class ContextView extends Disposable {
this.toDisposeOnSetContainer = toDisposeOnSetContainer;
}
this.useFixedPosition = useFixedPosition;
}
show(delegate: IDelegate): void {
@@ -254,10 +258,11 @@ export class ContextView extends Disposable {
DOM.removeClasses(this.view, 'top', 'bottom', 'left', 'right');
DOM.addClass(this.view, anchorPosition === AnchorPosition.BELOW ? 'bottom' : 'top');
DOM.addClass(this.view, anchorAlignment === AnchorAlignment.LEFT ? 'left' : 'right');
DOM.toggleClass(this.view, 'fixed', this.useFixedPosition);
const containerPosition = DOM.getDomNodePagePosition(this.container!);
this.view.style.top = `${top - containerPosition.top}px`;
this.view.style.left = `${left - containerPosition.left}px`;
this.view.style.top = `${top - (this.useFixedPosition ? DOM.getDomNodePagePosition(this.view).top : containerPosition.top)}px`;
this.view.style.left = `${left - (this.useFixedPosition ? DOM.getDomNodePagePosition(this.view).left : containerPosition.left)}px`;
this.view.style.width = 'initial';
}

View File

@@ -14,6 +14,7 @@ import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes';
import { EventHelper, EventType, removeClass, addClass, append, $, addDisposableListener, addClasses } from 'vs/base/browser/dom';
import { IContextMenuDelegate } from 'vs/base/browser/contextmenu';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { Emitter } from 'vs/base/common/event';
export interface ILabelRenderer {
(container: HTMLElement): IDisposable | null;
@@ -29,7 +30,10 @@ export class BaseDropdown extends ActionRunner {
private boxContainer?: HTMLElement;
private _label?: HTMLElement;
private contents?: HTMLElement;
private visible: boolean | undefined;
private _onDidChangeVisibility = new Emitter<boolean>();
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
constructor(container: HTMLElement, options: IBaseDropdownOptions) {
super();
@@ -48,7 +52,7 @@ export class BaseDropdown extends ActionRunner {
}
for (const event of [EventType.CLICK, EventType.MOUSE_DOWN, GestureEventType.Tap]) {
this._register(addDisposableListener(this._label, event, e => EventHelper.stop(e, true))); // prevent default click behaviour to trigger
this._register(addDisposableListener(this.element, event, e => EventHelper.stop(e, true))); // prevent default click behaviour to trigger
}
for (const event of [EventType.MOUSE_DOWN, GestureEventType.Tap]) {
@@ -101,11 +105,17 @@ export class BaseDropdown extends ActionRunner {
}
show(): void {
this.visible = true;
if (!this.visible) {
this.visible = true;
this._onDidChangeVisibility.fire(true);
}
}
hide(): void {
this.visible = false;
if (this.visible) {
this.visible = false;
this._onDidChangeVisibility.fire(false);
}
}
isVisible(): boolean {
@@ -256,7 +266,8 @@ export class DropdownMenu extends BaseDropdown {
getMenuClassName: () => this.menuClassName,
onHide: () => this.onHide(),
actionRunner: this.menuOptions ? this.menuOptions.actionRunner : undefined,
anchorAlignment: this.menuOptions ? this.menuOptions.anchorAlignment : AnchorAlignment.LEFT
anchorAlignment: this.menuOptions ? this.menuOptions.anchorAlignment : AnchorAlignment.LEFT,
anchorAsContainer: true
});
}
@@ -303,6 +314,7 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem {
this.element.tabIndex = 0;
this.element.setAttribute('role', 'button');
this.element.setAttribute('aria-haspopup', 'true');
this.element.setAttribute('aria-expanded', 'false');
this.element.title = this._action.label || '';
return null;
@@ -321,6 +333,7 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem {
}
this.dropdownMenu = this._register(new DropdownMenu(container, options));
this._register(this.dropdownMenu.onDidChangeVisibility(visible => this.element?.setAttribute('aria-expanded', `${visible}`)));
this.dropdownMenu.menuOptions = {
actionViewItemProvider: this.actionViewItemProvider,

View File

@@ -380,7 +380,7 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
throw new Error('Invalid index');
}
this.splitview.addView(node, size, index);
this.splitview.addView(node, size, index, skipLayout);
this._addChild(node, index);
this.onDidChildrenChange();
}
@@ -791,7 +791,7 @@ function flipNode<T extends Node>(node: T, size: number, orthogonalSize: number)
newSize += size - totalSize;
}
result.addChild(flipNode(child, orthogonalSize, newSize), newSize, 0);
result.addChild(flipNode(child, orthogonalSize, newSize), newSize, 0, true);
}
return result as T;

View 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.
*--------------------------------------------------------------------------------------------*/
.monaco-hover {
cursor: default;
position: absolute;
overflow: hidden;
z-index: 50;
user-select: text;
-webkit-user-select: text;
-ms-user-select: text;
box-sizing: initial;
animation: fadein 100ms linear;
line-height: 1.5em;
}
.monaco-hover.hidden {
display: none;
}
.monaco-hover .hover-contents {
padding: 4px 8px;
}
.monaco-hover .markdown-hover > .hover-contents:not(.code-hover-contents) {
max-width: 500px;
word-wrap: break-word;
}
.monaco-hover .markdown-hover > .hover-contents:not(.code-hover-contents) hr {
/* This is a strange rule but it avoids https://github.com/microsoft/vscode/issues/96795, just 100vw on its own caused the actual hover width to increase */
min-width: calc(100% + 100vw);
}
.monaco-hover p,
.monaco-hover ul {
margin: 8px 0;
}
.monaco-hover code {
font-family: var(--monaco-monospace-font);
}
.monaco-hover hr {
margin-top: 4px;
margin-bottom: -6px;
margin-left: -10px;
margin-right: -10px;
height: 1px;
}
.monaco-hover p:first-child,
.monaco-hover ul:first-child {
margin-top: 0;
}
.monaco-hover p:last-child,
.monaco-hover ul:last-child {
margin-bottom: 0;
}
/* MarkupContent Layout */
.monaco-hover ul {
padding-left: 20px;
}
.monaco-hover ol {
padding-left: 20px;
}
.monaco-hover li > p {
margin-bottom: 0;
}
.monaco-hover li > ul {
margin-top: 0;
}
.monaco-hover code {
border-radius: 3px;
padding: 0 0.4em;
}
.monaco-hover .monaco-tokenized-source {
white-space: pre-wrap;
word-break: break-all;
}
.monaco-hover .hover-row.status-bar {
font-size: 12px;
line-height: 22px;
}
.monaco-hover .hover-row.status-bar .actions {
display: flex;
padding: 0px 8px;
}
.monaco-hover .hover-row.status-bar .actions .action-container {
margin-right: 16px;
cursor: pointer;
}
.monaco-hover .hover-row.status-bar .actions .action-container .action .icon {
padding-right: 4px;
}
.monaco-hover .markdown-hover .hover-contents .codicon {
color: inherit;
font-size: inherit;
vertical-align: middle;
}
.monaco-hover .hover-contents a.code-link:before {
content: '(';
}
.monaco-hover .hover-contents a.code-link:after {
content: ')';
}
.monaco-hover .hover-contents a.code-link {
color: inherit;
}
.monaco-hover .hover-contents a.code-link > span {
text-decoration: underline;
/** Hack to force underline to show **/
border-bottom: 1px solid transparent;
text-underline-position: under;
}

View File

@@ -0,0 +1,54 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./hover';
import * as dom from 'vs/base/browser/dom';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
const $ = dom.$;
export class HoverWidget extends Disposable {
public readonly containerDomNode: HTMLElement;
public readonly contentsDomNode: HTMLElement;
private readonly _scrollbar: DomScrollableElement;
constructor() {
super();
this.containerDomNode = document.createElement('div');
this.containerDomNode.className = 'monaco-hover';
this.containerDomNode.tabIndex = 0;
this.containerDomNode.setAttribute('role', 'tooltip');
this.contentsDomNode = document.createElement('div');
this.contentsDomNode.className = 'monaco-hover-content';
this._scrollbar = this._register(new DomScrollableElement(this.contentsDomNode, {}));
this.containerDomNode.appendChild(this._scrollbar.getDomNode());
}
public onContentsChanged(): void {
this._scrollbar.scanDomNode();
}
}
export function renderHoverAction(parent: HTMLElement, actionOptions: { label: string, iconClass?: string, run: (target: HTMLElement) => void, commandId: string }, keybindingLabel: string | null): IDisposable {
const actionContainer = dom.append(parent, $('div.action-container'));
const action = dom.append(actionContainer, $('a.action'));
action.setAttribute('href', '#');
action.setAttribute('role', 'button');
if (actionOptions.iconClass) {
dom.append(action, $(`span.icon.${actionOptions.iconClass}`));
}
const label = dom.append(action, $('span'));
label.textContent = keybindingLabel ? `${actionOptions.label} (${keybindingLabel})` : actionOptions.label;
return dom.addDisposableListener(actionContainer, dom.EventType.CLICK, e => {
e.stopPropagation();
e.preventDefault();
actionOptions.run(actionContainer);
});
}

View File

@@ -165,7 +165,7 @@ export class PagedList<T> implements IDisposable {
}
get onDidChangeFocus(): Event<IListEvent<T>> {
return Event.map(this.list.onDidChangeFocus, ({ elements, indexes }) => ({ elements: elements.map(e => this._model.get(e)), indexes }));
return Event.map(this.list.onDidChangeFocus, ({ elements, indexes, browserEvent }) => ({ elements: elements.map(e => this._model.get(e)), indexes, browserEvent }));
}
get onDidOpen(): Event<IListEvent<T>> {
@@ -173,11 +173,11 @@ export class PagedList<T> implements IDisposable {
}
get onDidChangeSelection(): Event<IListEvent<T>> {
return Event.map(this.list.onDidChangeSelection, ({ elements, indexes }) => ({ elements: elements.map(e => this._model.get(e)), indexes }));
return Event.map(this.list.onDidChangeSelection, ({ elements, indexes, browserEvent }) => ({ elements: elements.map(e => this._model.get(e)), indexes, browserEvent }));
}
get onPin(): Event<IListEvent<T>> {
return Event.map(this.list.onDidPin, ({ elements, indexes }) => ({ elements: elements.map(e => this._model.get(e)), indexes }));
return Event.map(this.list.onDidPin, ({ elements, indexes, browserEvent }) => ({ elements: elements.map(e => this._model.get(e)), indexes, browserEvent }));
}
get onContextMenu(): Event<IListContextMenuEvent<T>> {

View File

@@ -9,8 +9,8 @@ import { Gesture, EventType as TouchEventType, GestureEvent } from 'vs/base/brow
import * as DOM from 'vs/base/browser/dom';
import { Event, Emitter } from 'vs/base/common/event';
import { domEvent } from 'vs/base/browser/event';
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { ScrollEvent, ScrollbarVisibility, INewScrollDimensions } from 'vs/base/common/scrollable';
import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { ScrollEvent, ScrollbarVisibility, INewScrollDimensions, Scrollable } from 'vs/base/common/scrollable';
import { RangeMap, shift } from './rangeMap';
import { IListVirtualDelegate, IListRenderer, IListMouseEvent, IListTouchEvent, IListGestureEvent, IListDragEvent, IListDragAndDrop, ListDragOverEffect } from './list';
import { RowCache, IRow } from './rowCache';
@@ -44,11 +44,16 @@ export interface IListViewDragAndDrop<T> extends IListDragAndDrop<T> {
export interface IListViewAccessibilityProvider<T> {
getSetSize?(element: T, index: number, listLength: number): number;
getPosInSet?(element: T, index: number): number;
getRole?(element: T): string;
getRole?(element: T): string | undefined;
isChecked?(element: T): boolean | undefined;
}
export interface IListViewOptions<T> {
export interface IListViewOptionsUpdate {
readonly additionalScrollHeight?: number;
readonly smoothScrolling?: boolean;
}
export interface IListViewOptions<T> extends IListViewOptionsUpdate {
readonly dnd?: IListViewDragAndDrop<T>;
readonly useShadows?: boolean;
readonly verticalScrollMode?: ScrollbarVisibility;
@@ -58,7 +63,6 @@ export interface IListViewOptions<T> {
readonly mouseSupport?: boolean;
readonly horizontalScrolling?: boolean;
readonly accessibilityProvider?: IListViewAccessibilityProvider<T>;
readonly additionalScrollHeight?: number;
readonly transformOptimization?: boolean;
}
@@ -158,7 +162,7 @@ class ListViewAccessibilityProvider<T> implements Required<IListViewAccessibilit
readonly getSetSize: (element: any, index: number, listLength: number) => number;
readonly getPosInSet: (element: any, index: number) => number;
readonly getRole: (element: T) => string;
readonly getRole: (element: T) => string | undefined;
readonly isChecked: (element: T) => boolean | undefined;
constructor(accessibilityProvider?: IListViewAccessibilityProvider<T>) {
@@ -204,7 +208,8 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
private lastRenderHeight: number;
private renderWidth = 0;
private rowsContainer: HTMLElement;
private scrollableElement: ScrollableElement;
private scrollable: Scrollable;
private scrollableElement: SmoothScrollableElement;
private _scrollHeight: number = 0;
private scrollableElementUpdateDisposable: IDisposable | null = null;
private scrollableElementWidthDelayer = new Delayer<void>(50);
@@ -278,18 +283,20 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
this.rowsContainer = document.createElement('div');
this.rowsContainer.className = 'monaco-list-rows';
if (options.transformOptimization) {
const transformOptimization = getOrDefault(options, o => o.transformOptimization, DefaultOptions.transformOptimization);
if (transformOptimization) {
this.rowsContainer.style.transform = 'translate3d(0px, 0px, 0px)';
}
this.disposables.add(Gesture.addTarget(this.rowsContainer));
this.scrollableElement = this.disposables.add(new ScrollableElement(this.rowsContainer, {
this.scrollable = new Scrollable(getOrDefault(options, o => o.smoothScrolling, false) ? 125 : 0, cb => DOM.scheduleAtNextAnimationFrame(cb));
this.scrollableElement = this.disposables.add(new SmoothScrollableElement(this.rowsContainer, {
alwaysConsumeMouseWheel: true,
horizontal: this.horizontalScrolling ? ScrollbarVisibility.Auto : ScrollbarVisibility.Hidden,
vertical: getOrDefault(options, o => o.verticalScrollMode, DefaultOptions.verticalScrollMode),
useShadows: getOrDefault(options, o => o.useShadows, DefaultOptions.useShadows)
}));
useShadows: getOrDefault(options, o => o.useShadows, DefaultOptions.useShadows),
}, this.scrollable));
this.domNode.appendChild(this.scrollableElement.getDomNode());
container.appendChild(this.domNode);
@@ -319,6 +326,10 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
if (options.additionalScrollHeight !== undefined) {
this.additionalScrollHeight = options.additionalScrollHeight;
}
if (options.smoothScrolling !== undefined) {
this.scrollable.setSmoothScrollDuration(options.smoothScrolling ? 125 : 0);
}
}
triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) {
@@ -651,7 +662,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
if (!item.row) {
item.row = this.cache.alloc(item.templateId);
const role = this.accessibilityProvider.getRole(item.element);
const role = this.accessibilityProvider.getRole(item.element) || 'listitem';
item.row!.domNode!.setAttribute('role', role);
const checked = this.accessibilityProvider.isChecked(item.element);
if (typeof checked !== 'undefined') {

View File

@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./list';
import { localize } from 'vs/nls';
import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
import { isNumber } from 'vs/base/common/types';
import { range, firstIndex, binarySearch } from 'vs/base/common/arrays';
@@ -17,7 +16,7 @@ import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardE
import { Event, Emitter, EventBufferer } from 'vs/base/common/event';
import { domEvent } from 'vs/base/browser/event';
import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop, IListDragOverReaction, ListError, IKeyboardNavigationDelegate } from './list';
import { ListView, IListViewOptions, IListViewDragAndDrop, IListViewAccessibilityProvider } from './listView';
import { ListView, IListViewOptions, IListViewDragAndDrop, IListViewAccessibilityProvider, IListViewOptionsUpdate } from './listView';
import { Color } from 'vs/base/common/color';
import { mixin } from 'vs/base/common/objects';
import { ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable';
@@ -26,6 +25,7 @@ import { CombinedSpliceable } from 'vs/base/browser/ui/list/splice';
import { clamp } from 'vs/base/common/numbers';
import { matchesPrefix } from 'vs/base/common/filters';
import { IDragAndDropData } from 'vs/base/browser/dnd';
import { alert } from 'vs/base/browser/ui/aria/aria';
interface ITraitChangeEvent {
indexes: number[];
@@ -344,6 +344,7 @@ class TypeLabelController<T> implements IDisposable {
private automaticKeyboardNavigation = true;
private triggered = false;
private previouslyFocused = -1;
private readonly enabledDisposables = new DisposableStore();
private readonly disposables = new DisposableStore();
@@ -393,6 +394,7 @@ class TypeLabelController<T> implements IDisposable {
const onInput = Event.reduce<string | null, string | null>(Event.any(onChar, onClear), (r, i) => i === null ? null : ((r || '') + i));
onInput(this.onInput, this, this.enabledDisposables);
onClear(this.onClear, this, this.enabledDisposables);
this.enabled = true;
this.triggered = false;
@@ -408,6 +410,19 @@ class TypeLabelController<T> implements IDisposable {
this.triggered = false;
}
private onClear(): void {
const focus = this.list.getFocus();
if (focus.length > 0 && focus[0] === this.previouslyFocused) {
// List: re-anounce element on typing end since typed keys will interupt aria label of focused element
// Do not announce if there was a focus change at the end to prevent duplication https://github.com/microsoft/vscode/issues/95961
const ariaLabel = this.list.options.accessibilityProvider?.getAriaLabel(this.list.element(focus[0]));
if (ariaLabel) {
alert(ariaLabel);
}
}
this.previouslyFocused = -1;
}
private onInput(word: string | null): void {
if (!word) {
this.state = TypeLabelControllerState.Idle;
@@ -426,6 +441,7 @@ class TypeLabelController<T> implements IDisposable {
const labelStr = label && label.toString();
if (typeof labelStr === 'undefined' || matchesPrefix(word, labelStr)) {
this.previouslyFocused = start;
this.list.setFocus([index]);
this.list.reveal(index);
return;
@@ -1091,10 +1107,9 @@ class ListViewDragAndDrop<T> implements IListViewDragAndDrop<T> {
}
}
export interface IListOptionsUpdate {
export interface IListOptionsUpdate extends IListViewOptionsUpdate {
readonly enableKeyboardNavigation?: boolean;
readonly automaticKeyboardNavigation?: boolean;
readonly additionalScrollHeight?: number;
}
export class List<T> implements ISpliceable<T>, IDisposable {
@@ -1272,9 +1287,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
this.typeLabelController.updateOptions(this._options);
}
if (optionsUpdate.additionalScrollHeight !== undefined) {
this.view.updateOptions(optionsUpdate);
}
this.view.updateOptions(optionsUpdate);
}
get options(): IListOptions<T> {
@@ -1363,7 +1376,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
set ariaLabel(value: string) {
this._ariaLabel = value;
this.view.domNode.setAttribute('aria-label', localize('aria list', "{0}. Use the navigation keys to navigate.", value));
this.view.domNode.setAttribute('aria-label', value);
}
domFocus(): void {

View File

@@ -88,6 +88,9 @@
padding: 0.5em 0 0 0;
margin-bottom: 0.5em;
width: 100%;
height: 0px !important;
margin-left: .8em !important;
margin-right: .8em !important;
}
.monaco-menu .monaco-action-bar.vertical .action-label.separator.text {

View File

@@ -41,6 +41,7 @@ export interface IMenuOptions {
enableMnemonics?: boolean;
anchorAlignment?: AnchorAlignment;
expandDirection?: Direction;
useEventAsContext?: boolean;
}
export interface IMenuStyles {
@@ -316,7 +317,7 @@ export class Menu extends ActionBar {
return menuActionViewItem;
} else {
const menuItemOptions: IMenuItemOptions = { enableMnemonics: options.enableMnemonics };
const menuItemOptions: IMenuItemOptions = { enableMnemonics: options.enableMnemonics, useEventAsContext: options.useEventAsContext };
if (options.getKeyBinding) {
const keybinding = options.getKeyBinding(action);
if (keybinding) {

View File

@@ -140,8 +140,8 @@ export class MenuBar extends Disposable {
eventHandled = false;
}
// Never allow default tab behavior
if (event.equals(KeyCode.Tab | KeyMod.Shift) || event.equals(KeyCode.Tab)) {
// Never allow default tab behavior when not compact
if (this.options.compactMode === undefined && (event.equals(KeyCode.Tab | KeyMod.Shift) || event.equals(KeyCode.Tab))) {
event.preventDefault();
}
@@ -315,7 +315,7 @@ export class MenuBar extends Disposable {
createOverflowMenu(): void {
const label = this.options.compactMode !== undefined ? nls.localize('mAppMenu', 'Application Menu') : nls.localize('mMore', 'More');
const title = this.options.compactMode !== undefined ? label : undefined;
const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': -1, 'aria-label': label, 'title': title, 'aria-haspopup': true });
const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': this.options.compactMode !== undefined ? 0 : -1, 'aria-label': label, 'title': title, 'aria-haspopup': true });
const titleElement = $('div.menubar-menu-title.toolbar-toggle-more' + menuBarMoreIcon.cssSelector, { 'role': 'none', 'aria-hidden': true });
buttonElement.appendChild(titleElement);
@@ -326,7 +326,7 @@ export class MenuBar extends Disposable {
let event = new StandardKeyboardEvent(e as KeyboardEvent);
let eventHandled = true;
if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter)) && !this.isOpen) {
if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter) || (this.options.compactMode !== undefined && event.equals(KeyCode.Space))) && !this.isOpen) {
this.focusedMenu = { index: MenuBar.OVERFLOW_INDEX };
this.openedViaKeyboard = true;
this.focusState = MenubarState.OPEN;
@@ -945,7 +945,8 @@ export class MenuBar extends Disposable {
actionRunner: this.actionRunner,
enableMnemonics: this.options.alwaysOnMnemonics || (this.mnemonicsInUse && this.options.enableMnemonics),
ariaLabel: withNullAsUndefined(customMenu.buttonElement.getAttribute('aria-label')),
expandDirection: this.options.compactMode !== undefined ? this.options.compactMode : Direction.Right
expandDirection: this.options.compactMode !== undefined ? this.options.compactMode : Direction.Right,
useEventAsContext: true
};
let menuWidget = this._register(new Menu(menuHolder, customMenu.actions, menuOptions));

View File

@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-mouse-cursor-text {
cursor: text;
}
/* The following selector looks a bit funny, but that is needed to cover all the workbench and the editor!! */
.vs-dark .mac .monaco-mouse-cursor-text, .hc-black .mac .monaco-mouse-cursor-text,
.vs-dark.mac .monaco-mouse-cursor-text, .hc-black.mac .monaco-mouse-cursor-text {
cursor: -webkit-image-set(url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAL0lEQVQoz2NgCD3x//9/BhBYBWdhgFVAiVW4JBFKGIa4AqD0//9D3pt4I4tAdAMAHTQ/j5Zom30AAAAASUVORK5CYII=') 1x, url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAAz0lEQVRIx2NgYGBY/R8I/vx5eelX3n82IJ9FxGf6tksvf/8FiTMQAcAGQMDvSwu09abffY8QYSAScNk45G198eX//yev73/4///701eh//kZSARckrNBRvz//+8+6ZohwCzjGNjdgQxkAg7B9WADeBjIBqtJCbhRA0YNoIkBSNmaPEMoNmA0FkYNoFKhapJ6FGyAH3nauaSmPfwI0v/3OukVi0CIZ+F25KrtYcx/CTIy0e+rC7R1Z4KMICVTQQ14feVXIbR695u14+Ir4gwAAD49E54wc1kWAAAAAElFTkSuQmCC') 2x) 5 8, text;
}

View File

@@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./mouseCursor';
export const MOUSE_CURSOR_TEXT_CSS_CLASS_NAME = `monaco-mouse-cursor-text`;

View File

@@ -13,13 +13,6 @@
pointer-events: none;
}
.monaco-sash.vertical {
cursor: ew-resize;
top: 0;
width: 4px;
height: 100%;
}
.monaco-sash.mac.vertical {
cursor: col-resize;
}
@@ -32,13 +25,6 @@
cursor: w-resize;
}
.monaco-sash.horizontal {
cursor: ns-resize;
left: 0;
width: 100%;
height: 4px;
}
.monaco-sash.mac.horizontal {
cursor: row-resize;
}
@@ -51,52 +37,11 @@
cursor: n-resize;
}
.monaco-sash:not(.disabled).orthogonal-start::before,
.monaco-sash:not(.disabled).orthogonal-end::after {
content: ' ';
height: 8px;
width: 8px;
z-index: 100;
display: block;
cursor: all-scroll;
position: absolute;
}
.monaco-sash.orthogonal-start.vertical::before {
left: -2px;
top: -4px;
}
.monaco-sash.orthogonal-end.vertical::after {
left: -2px;
bottom: -4px;
}
.monaco-sash.orthogonal-start.horizontal::before {
top: -2px;
left: -4px;
}
.monaco-sash.orthogonal-end.horizontal::after {
top: -2px;
right: -4px;
}
.monaco-sash.disabled {
cursor: default !important;
pointer-events: none !important;
}
/** Touch **/
.monaco-sash.touch.vertical {
width: 20px;
}
.monaco-sash.touch.horizontal {
height: 20px;
}
/** Debug **/
.monaco-sash.debug {
@@ -110,4 +55,4 @@
.monaco-sash.debug:not(.disabled).orthogonal-start::before,
.monaco-sash.debug:not(.disabled).orthogonal-end::after {
background: red;
}
}

View File

@@ -5,7 +5,6 @@
import 'vs/css!./sash';
import { IDisposable, dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { isIPad } from 'vs/base/browser/browser';
import { isMacintosh } from 'vs/base/common/platform';
import * as types from 'vs/base/common/types';
import { EventType, GestureEvent, Gesture } from 'vs/base/browser/touch';
@@ -39,9 +38,18 @@ export interface ISashEvent {
}
export interface ISashOptions {
orientation?: Orientation;
orthogonalStartSash?: Sash;
orthogonalEndSash?: Sash;
readonly orientation: Orientation;
readonly orthogonalStartSash?: Sash;
readonly orthogonalEndSash?: Sash;
readonly size?: number;
}
export interface IVerticalSashOptions extends ISashOptions {
readonly orientation: Orientation.VERTICAL;
}
export interface IHorizontalSashOptions extends ISashOptions {
readonly orientation: Orientation.HORIZONTAL;
}
export const enum Orientation {
@@ -56,12 +64,20 @@ export const enum SashState {
Enabled
}
let globalSize = 4;
const onDidChangeGlobalSize = new Emitter<number>();
export function setGlobalSashSize(size: number): void {
globalSize = size;
onDidChangeGlobalSize.fire(size);
}
export class Sash extends Disposable {
private el: HTMLElement;
private layoutProvider: ISashLayoutProvider;
private hidden: boolean;
private orientation!: Orientation;
private size: number;
private _state: SashState = SashState.Enabled;
get state(): SashState { return this._state; }
@@ -127,7 +143,9 @@ export class Sash extends Disposable {
this._orthogonalEndSash = sash;
}
constructor(container: HTMLElement, layoutProvider: ISashLayoutProvider, options: ISashOptions = {}) {
constructor(container: HTMLElement, layoutProvider: IVerticalSashLayoutProvider, options: ISashOptions);
constructor(container: HTMLElement, layoutProvider: IHorizontalSashLayoutProvider, options: ISashOptions);
constructor(container: HTMLElement, layoutProvider: ISashLayoutProvider, options: ISashOptions) {
super();
this.el = append(container, $('.monaco-sash'));
@@ -142,12 +160,21 @@ export class Sash extends Disposable {
this._register(Gesture.addTarget(this.el));
this._register(domEvent(this.el, EventType.Start)(this.onTouchStart, this));
if (isIPad) {
// see also https://ux.stackexchange.com/questions/39023/what-is-the-optimum-button-size-of-touch-screen-applications
addClass(this.el, 'touch');
}
if (typeof options.size === 'number') {
this.size = options.size;
this.setOrientation(options.orientation || Orientation.VERTICAL);
if (options.orientation === Orientation.VERTICAL) {
this.el.style.width = `${this.size}px`;
} else {
this.el.style.height = `${this.size}px`;
}
} else {
this.size = globalSize;
this._register(onDidChangeGlobalSize.event(size => {
this.size = size;
this.layout();
}));
}
this.hidden = false;
this.layoutProvider = layoutProvider;
@@ -155,11 +182,7 @@ export class Sash extends Disposable {
this.orthogonalStartSash = options.orthogonalStartSash;
this.orthogonalEndSash = options.orthogonalEndSash;
toggleClass(this.el, 'debug', DEBUG);
}
setOrientation(orientation: Orientation): void {
this.orientation = orientation;
this.orientation = options.orientation || Orientation.VERTICAL;
if (this.orientation === Orientation.HORIZONTAL) {
addClass(this.el, 'horizontal');
@@ -169,9 +192,9 @@ export class Sash extends Disposable {
addClass(this.el, 'vertical');
}
if (this.layoutProvider) {
this.layout();
}
toggleClass(this.el, 'debug', DEBUG);
this.layout();
}
private onMouseDown(e: MouseEvent): void {
@@ -331,11 +354,9 @@ export class Sash extends Disposable {
}
layout(): void {
const size = isIPad ? 20 : 4;
if (this.orientation === Orientation.VERTICAL) {
const verticalProvider = (<IVerticalSashLayoutProvider>this.layoutProvider);
this.el.style.left = verticalProvider.getVerticalSashLeft(this) - (size / 2) + 'px';
this.el.style.left = verticalProvider.getVerticalSashLeft(this) - (this.size / 2) + 'px';
if (verticalProvider.getVerticalSashTop) {
this.el.style.top = verticalProvider.getVerticalSashTop(this) + 'px';
@@ -346,7 +367,7 @@ export class Sash extends Disposable {
}
} else {
const horizontalProvider = (<IHorizontalSashLayoutProvider>this.layoutProvider);
this.el.style.top = horizontalProvider.getHorizontalSashTop(this) - (size / 2) + 'px';
this.el.style.top = horizontalProvider.getHorizontalSashTop(this) - (this.size / 2) + 'px';
if (horizontalProvider.getHorizontalSashLeft) {
this.el.style.left = horizontalProvider.getHorizontalSashLeft(this) + 'px';
@@ -384,15 +405,15 @@ export class Sash extends Disposable {
private getOrthogonalSash(e: MouseEvent): Sash | undefined {
if (this.orientation === Orientation.VERTICAL) {
if (e.offsetY <= 4) {
if (e.offsetY <= this.size) {
return this.orthogonalStartSash;
} else if (e.offsetY >= this.el.clientHeight - 4) {
} else if (e.offsetY >= this.el.clientHeight - this.size) {
return this.orthogonalEndSash;
}
} else {
if (e.offsetX <= 4) {
if (e.offsetX <= this.size) {
return this.orthogonalStartSash;
} else if (e.offsetX >= this.el.clientWidth - 4) {
} else if (e.offsetX >= this.el.clientWidth - this.size) {
return this.orthogonalEndSash;
}
}

View File

@@ -533,6 +533,14 @@ export class SmoothScrollableElement extends AbstractScrollableElement {
super(element, options, scrollable);
}
public setScrollPosition(update: INewScrollPosition): void {
this._scrollable.setScrollPositionNow(update);
}
public getScrollPosition(): IScrollPosition {
return this._scrollable.getCurrentScrollPosition();
}
}
export class DomScrollableElement extends ScrollableElement {

View File

@@ -6,3 +6,9 @@
.monaco-select-box {
width: 100%;
}
.monaco-select-box-dropdown-container {
font-size: 13px;
font-weight: normal;
text-transform: none;
}

View File

@@ -40,6 +40,7 @@ export interface ISelectBoxOptions {
useCustomDrawn?: boolean;
ariaLabel?: string;
minBottomMargin?: number;
optionsAsChildren?: boolean;
}
// Utilize optionItem interface to capture all option parameters

View File

@@ -90,8 +90,8 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
private _isVisible: boolean;
private selectBoxOptions: ISelectBoxOptions;
public selectElement: HTMLSelectElement; // {{SQL CARBON EDIT}}
public selectElement: HTMLSelectElement; // {{SQL CARBON EDIT}}
private container?: HTMLElement;
private options: ISelectOptionItem[] = [];
private selected: number;
private readonly _onDidSelect: Emitter<ISelectData>;
@@ -310,6 +310,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
}
public render(container: HTMLElement): void {
this.container = container;
dom.addClass(container, 'select-container');
container.appendChild(this.selectElement);
this.applyStyles();
@@ -454,7 +455,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
dom.toggleClass(this.selectElement, 'synthetic-focus', false);
},
anchorPosition: this._dropDownPosition
});
}, this.selectBoxOptions.optionsAsChildren ? this.container : undefined);
// Hide so we can relay out
this._isVisible = true;
@@ -469,7 +470,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
dom.toggleClass(this.selectElement, 'synthetic-focus', false);
},
anchorPosition: this._dropDownPosition
});
}, this.selectBoxOptions.optionsAsChildren ? this.container : undefined);
// Track initial selection the case user escape, blur
this._currentSelection = this.selected;
@@ -739,7 +740,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
mouseSupport: false,
accessibilityProvider: {
getAriaLabel: (element) => element.text,
getWidgetAriaLabel: () => localize('selectBox', "Select Box"),
getWidgetAriaLabel: () => localize({ key: 'selectBox', comment: ['Behave like native select dropdown element.'] }, "Select Box"),
getRole: () => 'option',
getWidgetRole: () => 'listbox'
}

View File

@@ -16,7 +16,12 @@
flex-direction: column;
}
.monaco-pane-view .pane.horizontal:not(.expanded) {
flex-direction: row;
}
.monaco-pane-view .pane > .pane-header {
height: 22px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
@@ -26,6 +31,12 @@
align-items: center;
}
.monaco-pane-view .pane.horizontal:not(.expanded) > .pane-header {
flex-direction: column;
height: 100%;
width: 22px;
}
.monaco-pane-view .pane > .pane-header > .twisties {
width: 20px;
display: flex;
@@ -36,6 +47,11 @@
flex-shrink: 0;
}
.monaco-pane-view .pane.horizontal:not(.expanded) > .pane-header > .twisties {
margin-top: 2px;
margin-bottom: 2px;
}
.monaco-pane-view .pane > .pane-header.expanded > .twisties::before {
transform: rotate(90deg);
}
@@ -132,6 +148,7 @@
width: 100%;
height: 100%;
min-height: 22px;
min-width: 19px;
pointer-events: none; /* very important to not take events away from the parent */
transition: opacity 150ms ease-out;

View File

@@ -106,7 +106,7 @@ export abstract class Pane extends Disposable implements IView {
get minimumSize(): number {
const headerSize = this.headerSize;
const expanded = !this.headerVisible || this.isExpanded();
const minimumBodySize = expanded ? this.minimumBodySize : this._orientation === Orientation.HORIZONTAL ? 50 : 0;
const minimumBodySize = expanded ? this.minimumBodySize : 0;
return headerSize + minimumBodySize;
}
@@ -114,7 +114,7 @@ export abstract class Pane extends Disposable implements IView {
get maximumSize(): number {
const headerSize = this.headerSize;
const expanded = !this.headerVisible || this.isExpanded();
const maximumBodySize = expanded ? this.maximumBodySize : this._orientation === Orientation.HORIZONTAL ? 50 : 0;
const maximumBodySize = expanded ? this.maximumBodySize : 0;
return headerSize + maximumBodySize;
}
@@ -126,7 +126,7 @@ export abstract class Pane extends Disposable implements IView {
this._expanded = typeof options.expanded === 'undefined' ? true : !!options.expanded;
this._orientation = typeof options.orientation === 'undefined' ? Orientation.VERTICAL : options.orientation;
this.ariaHeaderLabel = localize('viewSection', "{0} Section", options.title);
this._minimumBodySize = typeof options.minimumBodySize === 'number' ? options.minimumBodySize : 120;
this._minimumBodySize = typeof options.minimumBodySize === 'number' ? options.minimumBodySize : this._orientation === Orientation.HORIZONTAL ? 200 : 120;
this._maximumBodySize = typeof options.maximumBodySize === 'number' ? options.maximumBodySize : Number.POSITIVE_INFINITY;
this.element = $('.pane');
@@ -141,6 +141,10 @@ export abstract class Pane extends Disposable implements IView {
return false;
}
if (this.element) {
toggleClass(this.element, 'expanded', expanded);
}
this._expanded = !!expanded;
this.updateHeader();
@@ -185,12 +189,21 @@ export abstract class Pane extends Disposable implements IView {
this._orientation = orientation;
if (this.element) {
toggleClass(this.element, 'horizontal', this.orientation === Orientation.HORIZONTAL);
toggleClass(this.element, 'vertical', this.orientation === Orientation.VERTICAL);
}
if (this.header) {
this.updateHeader();
}
}
render(): void {
toggleClass(this.element, 'expanded', this.isExpanded());
toggleClass(this.element, 'horizontal', this.orientation === Orientation.HORIZONTAL);
toggleClass(this.element, 'vertical', this.orientation === Orientation.VERTICAL);
this.header = $('.pane-header');
append(this.element, this.header);
this.header.setAttribute('tabindex', '0');
@@ -250,8 +263,6 @@ export abstract class Pane extends Disposable implements IView {
style(styles: IPaneStyles): void {
this.styles = styles;
this.element.style.borderLeft = this.styles.leftBorder && this.orientation === Orientation.HORIZONTAL ? `1px solid ${this.styles.leftBorder}` : '';
if (!this.header) {
return;
}
@@ -262,7 +273,6 @@ export abstract class Pane extends Disposable implements IView {
protected updateHeader(): void {
const expanded = !this.headerVisible || this.isExpanded();
this.header.style.height = `${this.headerSize}px`;
this.header.style.lineHeight = `${this.headerSize}px`;
toggleClass(this.header, 'hidden', !this.headerVisible);
toggleClass(this.header, 'expanded', expanded);
@@ -272,6 +282,7 @@ export abstract class Pane extends Disposable implements IView {
this.header.style.backgroundColor = this.styles.headerBackground ? this.styles.headerBackground.toString() : '';
this.header.style.borderTop = this.styles.headerBorder && this.orientation === Orientation.VERTICAL ? `1px solid ${this.styles.headerBorder}` : '';
this._dropBackground = this.styles.dropBackground;
this.element.style.borderLeft = this.styles.leftBorder && this.orientation === Orientation.HORIZONTAL ? `1px solid ${this.styles.leftBorder}` : '';
}
protected abstract renderHeader(container: HTMLElement): void;

View File

@@ -331,8 +331,8 @@ export class SplitView<TLayoutContext = undefined> extends Disposable {
}
}
addView(view: IView<TLayoutContext>, size: number | Sizing, index = this.viewItems.length): void {
this.doAddView(view, size, index, false);
addView(view: IView<TLayoutContext>, size: number | Sizing, index = this.viewItems.length, skipLayout?: boolean): void {
this.doAddView(view, size, index, skipLayout);
}
removeView(index: number, sizing?: Sizing): IView<TLayoutContext> {
@@ -689,13 +689,17 @@ export class SplitView<TLayoutContext = undefined> extends Disposable {
// Add sash
if (this.viewItems.length > 1) {
const orientation = this.orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL;
const layoutProvider = this.orientation === Orientation.VERTICAL ? { getHorizontalSashTop: (sash: Sash) => this.getSashPosition(sash) } : { getVerticalSashLeft: (sash: Sash) => this.getSashPosition(sash) };
const sash = new Sash(this.sashContainer, layoutProvider, {
orientation,
orthogonalStartSash: this.orthogonalStartSash,
orthogonalEndSash: this.orthogonalEndSash
});
const sash = this.orientation === Orientation.VERTICAL
? new Sash(this.sashContainer, { getHorizontalSashTop: (sash: Sash) => this.getSashPosition(sash) }, {
orientation: Orientation.HORIZONTAL,
orthogonalStartSash: this.orthogonalStartSash,
orthogonalEndSash: this.orthogonalEndSash
})
: new Sash(this.sashContainer, { getVerticalSashLeft: (sash: Sash) => this.getSashPosition(sash) }, {
orientation: Orientation.VERTICAL,
orthogonalStartSash: this.orthogonalStartSash,
orthogonalEndSash: this.orthogonalEndSash
});
const sashEventMapper = this.orientation === Orientation.VERTICAL
? (e: IBaseSashEvent) => ({ sash, start: e.startY, current: e.currentY, alt: e.altKey })
@@ -951,7 +955,7 @@ export class SplitView<TLayoutContext = undefined> extends Disposable {
position += this.viewItems[i].size;
if (this.sashItems[i].sash === sash) {
return Math.min(position, this.contentSize - 2);
return position;
}
}

View File

@@ -707,7 +707,7 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
.map(e => new StandardKeyboardEvent(e))
.filter(this.keyboardNavigationEventFilter || (() => true))
.filter(() => this.automaticKeyboardNavigation || this.triggered)
.filter(e => this.keyboardNavigationDelegate.mightProducePrintableCharacter(e) || ((this.pattern.length > 0 || this.triggered) && ((e.keyCode === KeyCode.Escape || e.keyCode === KeyCode.Backspace) && !e.altKey && !e.ctrlKey && !e.metaKey) || (e.keyCode === KeyCode.Backspace && (isMacintosh ? (e.altKey && !e.metaKey) : e.ctrlKey) && !e.shiftKey)))
.filter(e => (this.keyboardNavigationDelegate.mightProducePrintableCharacter(e) && !(e.keyCode === KeyCode.DownArrow || e.keyCode === KeyCode.UpArrow || e.keyCode === KeyCode.LeftArrow || e.keyCode === KeyCode.RightArrow)) || ((this.pattern.length > 0 || this.triggered) && ((e.keyCode === KeyCode.Escape || e.keyCode === KeyCode.Backspace) && !e.altKey && !e.ctrlKey && !e.metaKey) || (e.keyCode === KeyCode.Backspace && (isMacintosh ? (e.altKey && !e.metaKey) : e.ctrlKey) && !e.shiftKey)))
.forEach(e => { e.stopPropagation(); e.preventDefault(); })
.event;
@@ -962,6 +962,7 @@ export interface IAbstractTreeOptionsUpdate extends ITreeRendererOptions {
readonly simpleKeyboardNavigation?: boolean;
readonly filterOnType?: boolean;
readonly openOnSingleClick?: boolean;
readonly smoothScrolling?: boolean;
}
export interface IAbstractTreeOptions<T, TFilterData = void> extends IAbstractTreeOptionsUpdate, IListOptions<T> {
@@ -1360,7 +1361,8 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
this.view.updateOptions({
enableKeyboardNavigation: this._options.simpleKeyboardNavigation,
automaticKeyboardNavigation: this._options.automaticKeyboardNavigation
automaticKeyboardNavigation: this._options.automaticKeyboardNavigation,
smoothScrolling: this._options.smoothScrolling
});
if (this.typeFilterController) {

View File

@@ -13,7 +13,7 @@ export class CollapseAllAction<TInput, T, TFilterData = void> extends Action {
super('vs.tree.collapse', nls.localize('collapse all', "Collapse All"), 'collapse-all', enabled);
}
async run(context?: any): Promise<any> {
async run(): Promise<any> {
this.viewer.collapseAll();
this.viewer.setSelection([]);
this.viewer.setFocus([]);

View File

@@ -764,7 +764,7 @@ export class IdleValue<T> {
this._handle.dispose();
}
getValue(): T {
get value(): T {
if (!this._didRun) {
this._handle.dispose();
this._executor();

View File

@@ -342,7 +342,17 @@ export const enum CharCode {
* Unicode Character 'LINE SEPARATOR' (U+2028)
* http://www.fileformat.info/info/unicode/char/2028/index.htm
*/
LINE_SEPARATOR_2028 = 8232,
LINE_SEPARATOR = 0x2028,
/**
* Unicode Character 'PARAGRAPH SEPARATOR' (U+2029)
* http://www.fileformat.info/info/unicode/char/2029/index.htm
*/
PARAGRAPH_SEPARATOR = 0x2029,
/**
* Unicode Character 'NEXT LINE' (U+0085)
* http://www.fileformat.info/info/unicode/char/0085/index.htm
*/
NEXT_LINE = 0x0085,
// http://www.fileformat.info/info/unicode/category/Sk/list.htm
U_CIRCUMFLEX = 0x005E, // U+005E CIRCUMFLEX
@@ -422,4 +432,4 @@ export const enum CharCode {
* http://www.fileformat.info/info/unicode/char/feff/index.htm
*/
UTF8_BOM = 65279
}
}

View File

@@ -244,7 +244,7 @@ export namespace Codicon {
export const collapseAll = new Codicon('collapse-all', { character: '\\eac5' });
export const colorMode = new Codicon('color-mode', { character: '\\eac6' });
export const commentDiscussion = new Codicon('comment-discussion', { character: '\\eac7' });
export const compareChanges = new Codicon('compare-changes', { character: '\\eac8' });
export const compareChanges = new Codicon('compare-changes', { character: '\\eafd' });
export const creditCard = new Codicon('credit-card', { character: '\\eac9' });
export const dash = new Codicon('dash', { character: '\\eacc' });
export const dashboard = new Codicon('dashboard', { character: '\\eacd' });
@@ -448,7 +448,6 @@ export namespace Codicon {
export const debugReverseContinue = new Codicon('debug-reverse-continue', { character: '\\eb8e' });
export const debugStepBack = new Codicon('debug-step-back', { character: '\\eb8f' });
export const debugRestartFrame = new Codicon('debug-restart-frame', { character: '\\eb90' });
export const debugAlternate = new Codicon('debug-alternate', { character: '\\eb91' });
export const callIncoming = new Codicon('call-incoming', { character: '\\eb92' });
export const callOutgoing = new Codicon('call-outgoing', { character: '\\eb93' });
export const menu = new Codicon('menu', { character: '\\eb94' });
@@ -465,8 +464,13 @@ export namespace Codicon {
export const syncIgnored = new Codicon('sync-ignored', { character: '\\eb9f' });
export const pinned = new Codicon('pinned', { character: '\\eba0' });
export const githubInverted = new Codicon('github-inverted', { character: '\\eba1' });
export const debugAlt2 = new Codicon('debug-alt-2', { character: '\\f101' });
export const debugAlt = new Codicon('debug-alt', { character: '\\f102' });
export const debugAlt = new Codicon('debug-alt', { character: '\\eb91' });
export const serverProcess = new Codicon('server-process', { character: '\\eba2' });
export const serverEnvironment = new Codicon('server-environment', { character: '\\eba3' });
export const pass = new Codicon('pass', { character: '\\eba4' });
export const stopCircle = new Codicon('stop-circle', { character: '\\eba5' });
export const playCircle = new Codicon('play-circle', { character: '\\eba6' });
export const record = new Codicon('record', { character: '\\eba7' });
}

View File

@@ -6,7 +6,12 @@
import { sep } from 'vs/base/common/path';
import { IdleValue } from 'vs/base/common/async';
const intlFileNameCollator: IdleValue<{ collator: Intl.Collator, collatorIsNumeric: boolean }> = new IdleValue(() => {
// 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()
// A collator with numeric sorting enabled, and no sensitivity to case or to accents
const intlFileNameCollatorBaseNumeric: IdleValue<{ collator: Intl.Collator, collatorIsNumeric: boolean }> = new IdleValue(() => {
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
return {
collator: collator,
@@ -14,20 +19,64 @@ const intlFileNameCollator: IdleValue<{ collator: Intl.Collator, collatorIsNumer
};
});
// A collator with numeric sorting enabled.
const intlFileNameCollatorNumeric: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => {
const collator = new Intl.Collator(undefined, { numeric: true });
return {
collator: collator
};
});
// A collator with numeric sorting enabled, and sensitivity to accents and diacritics but not case.
const intlFileNameCollatorNumericCaseInsenstive: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => {
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'accent' });
return {
collator: collator
};
});
export function compareFileNames(one: string | null, other: string | null, caseSensitive = false): number {
const a = one || '';
const b = other || '';
const result = intlFileNameCollator.getValue().collator.compare(a, b);
const result = intlFileNameCollatorBaseNumeric.value.collator.compare(a, b);
// Using the numeric option in the collator will
// make compare(`foo1`, `foo01`) === 0. We must disambiguate.
if (intlFileNameCollator.getValue().collatorIsNumeric && result === 0 && a !== b) {
if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && result === 0 && a !== b) {
return a < b ? -1 : 1;
}
return result;
}
/** Compares filenames by name then extension, sorting numbers numerically instead of alphabetically. */
export function compareFileNamesNumeric(one: string | null, other: string | null): number {
const [oneName, oneExtension] = extractNameAndExtension(one, true);
const [otherName, otherExtension] = extractNameAndExtension(other, true);
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator;
let result;
// Check for name differences, comparing numbers numerically instead of alphabetically.
result = compareAndDisambiguateByLength(collatorNumeric, oneName, otherName);
if (result !== 0) {
return result;
}
// Check for case insensitive extension differences, comparing numbers numerically instead of alphabetically.
result = compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension);
if (result !== 0) {
return result;
}
// Disambiguate the extension case if needed.
if (oneExtension !== otherExtension) {
return collatorNumeric.compare(oneExtension, otherExtension);
}
return 0;
}
const FileNameMatch = /^(.*?)(\.([^.]*))?$/;
export function noIntlCompareFileNames(one: string | null, other: string | null, caseSensitive = false): number {
@@ -54,19 +103,19 @@ export function compareFileExtensions(one: string | null, other: string | null):
const [oneName, oneExtension] = extractNameAndExtension(one);
const [otherName, otherExtension] = extractNameAndExtension(other);
let result = intlFileNameCollator.getValue().collator.compare(oneExtension, otherExtension);
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.
if (intlFileNameCollator.getValue().collatorIsNumeric && oneExtension !== otherExtension) {
if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && oneExtension !== otherExtension) {
return oneExtension < otherExtension ? -1 : 1;
}
// Extensions are equal, compare filenames
result = intlFileNameCollator.getValue().collator.compare(oneName, otherName);
result = intlFileNameCollatorBaseNumeric.value.collator.compare(oneName, otherName);
if (intlFileNameCollator.getValue().collatorIsNumeric && result === 0 && oneName !== otherName) {
if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && result === 0 && oneName !== otherName) {
return oneName < otherName ? -1 : 1;
}
}
@@ -74,10 +123,63 @@ export function compareFileExtensions(one: string | null, other: string | null):
return result;
}
function extractNameAndExtension(str?: string | null): [string, string] {
/** Compares filenames by extenson, then by name. Sorts numbers numerically, not alphabetically. */
export function compareFileExtensionsNumeric(one: string | null, other: string | null): number {
const [oneName, oneExtension] = extractNameAndExtension(one, true);
const [otherName, otherExtension] = extractNameAndExtension(other, true);
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator;
let result;
// Check for extension differences, ignoring differences in case and comparing numbers numerically.
result = compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension);
if (result !== 0) {
return result;
}
// Compare names.
result = compareAndDisambiguateByLength(collatorNumeric, oneName, otherName);
if (result !== 0) {
return result;
}
// Disambiguate extension case if needed.
if (oneExtension !== otherExtension) {
return collatorNumeric.compare(oneExtension, otherExtension);
}
return 0;
}
/** Extracts the name and extension from a full filename, with optional special handling for dotfiles */
function extractNameAndExtension(str?: string | null, dotfilesAsNames = false): [string, string] {
const match = str ? FileNameMatch.exec(str) as Array<string> : ([] as Array<string>);
return [(match && match[1]) || '', (match && match[3]) || ''];
let result: [string, string] = [(match && match[1]) || '', (match && match[3]) || ''];
// 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], ''];
}
return result;
}
function compareAndDisambiguateByLength(collator: Intl.Collator, one: string, other: string) {
// Check for differences
let result = collator.compare(one, other);
if (result !== 0) {
return result;
}
// In a numeric comparison, `foo1` and `foo01` will compare as equivalent.
// Disambiguate by sorting the shorter string first.
if (one.length !== other.length) {
return one.length < other.length ? -1 : 1;
}
return 0;
}
function comparePathComponents(one: string, other: string, caseSensitive = false): number {

View File

@@ -328,8 +328,8 @@ export namespace Event {
}
export interface NodeEventEmitter {
on(event: string | symbol, listener: Function): this;
removeListener(event: string | symbol, listener: Function): this;
on(event: string | symbol, listener: Function): unknown;
removeListener(event: string | symbol, listener: Function): unknown;
}
export function fromNodeEventEmitter<T>(emitter: NodeEventEmitter, eventName: string, map: (...args: any[]) => T = id => id): Event<T> {

View File

@@ -7,6 +7,7 @@ import { isWindows } from 'vs/base/common/platform';
import { startsWithIgnoreCase, equalsIgnoreCase, rtrim } from 'vs/base/common/strings';
import { CharCode } from 'vs/base/common/charCode';
import { sep, posix, isAbsolute, join, normalize } from 'vs/base/common/path';
import { isNumber } from 'vs/base/common/types';
export function isPathSeparator(code: number) {
return code === CharCode.Slash || code === CharCode.Backslash;
@@ -300,3 +301,38 @@ export function indexOfPath(path: string, candidate: string, ignoreCase: boolean
return path.indexOf(candidate);
}
export interface IPathWithLineAndColumn {
path: string;
line?: number;
column?: number;
}
export function parseLineAndColumnAware(rawPath: string): IPathWithLineAndColumn {
const segments = rawPath.split(':'); // C:\file.txt:<line>:<column>
let path: string | undefined = undefined;
let line: number | undefined = undefined;
let column: number | undefined = undefined;
segments.forEach(segment => {
const segmentAsNumber = Number(segment);
if (!isNumber(segmentAsNumber)) {
path = !!path ? [path, segment].join(':') : segment; // a colon can well be part of a path (e.g. C:\...)
} else if (line === undefined) {
line = segmentAsNumber;
} else if (column === undefined) {
column = segmentAsNumber;
}
});
if (!path) {
throw new Error('Format for `--goto` should be: `FILE:LINE(:COLUMN)`');
}
return {
path,
line: line !== undefined ? line : undefined,
column: column !== undefined ? column : line !== undefined ? 1 : undefined // if we have a line, make sure column is also set
};
}

View File

@@ -833,7 +833,7 @@ export interface IPreparedQuery extends IPreparedQueryPiece {
values: IPreparedQueryPiece[] | undefined;
/**
* Wether the query contains path separator(s) or not.
* Whether the query contains path separator(s) or not.
*/
containsPathSeparator: boolean;
}

View File

@@ -9,6 +9,7 @@ export function insane(
readonly allowedSchemes?: readonly string[],
readonly allowedTags?: readonly string[],
readonly allowedAttributes?: { readonly [key: string]: string[] },
readonly filter?: (token: { tag: string, attrs: { readonly [key: string]: string } }) => boolean,
},
strict?: boolean,
): string;

View File

@@ -5,6 +5,10 @@
export namespace Iterable {
export function is<T = any>(thing: any): thing is IterableIterator<T> {
return thing && typeof thing === 'object' && typeof thing[Symbol.iterator] === 'function';
}
const _empty: Iterable<any> = Object.freeze([]);
export function empty<T = any>(): Iterable<T> {
return _empty;

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { once } from 'vs/base/common/functional';
import { Iterable } from 'vs/base/common/iterator';
/**
* Enables logging of potentially leaked disposables.
@@ -54,26 +55,26 @@ export function isDisposable<E extends object>(thing: E): thing is E & IDisposab
export function dispose<T extends IDisposable>(disposable: T): T;
export function dispose<T extends IDisposable>(disposable: T | undefined): T | undefined;
export function dispose<T extends IDisposable, A extends IterableIterator<T> = IterableIterator<T>>(disposables: IterableIterator<T>): A;
export function dispose<T extends IDisposable>(disposables: Array<T>): Array<T>;
export function dispose<T extends IDisposable>(disposables: ReadonlyArray<T>): ReadonlyArray<T>;
export function dispose<T extends IDisposable>(disposables: T | T[] | undefined): T | T[] | undefined {
if (Array.isArray(disposables)) {
disposables.forEach(d => {
export function dispose<T extends IDisposable>(arg: T | IterableIterator<T> | undefined): any {
if (Iterable.is(arg)) {
for (let d of arg) {
if (d) {
markTracked(d);
d.dispose();
}
});
return [];
} else if (disposables) {
markTracked(disposables);
disposables.dispose();
return disposables;
} else {
return undefined;
}
return arg;
} else if (arg) {
markTracked(arg);
arg.dispose();
return arg;
}
}
export function combinedDisposable(...disposables: IDisposable[]): IDisposable {
disposables.forEach(markTracked);
return trackDisposable({ dispose: () => dispose(disposables) });

View File

@@ -206,7 +206,7 @@ export class UriIterator implements IKeyIterator<URI> {
cmp(a: string): number {
if (this._states[this._stateIdx] === UriIteratorState.Scheme) {
return compareSubstringIgnoreCase(a, this._value.scheme);
return compare(a, this._value.scheme);
} else if (this._states[this._stateIdx] === UriIteratorState.Authority) {
return compareSubstringIgnoreCase(a, this._value.authority);
} else if (this._states[this._stateIdx] === UriIteratorState.Path) {
@@ -486,11 +486,9 @@ export class ResourceMap<T> implements Map<URI, T> {
readonly [Symbol.toStringTag] = 'ResourceMap';
protected readonly map: Map<string, T>;
protected readonly ignoreCase?: boolean;
constructor() {
this.map = new Map<string, T>();
this.ignoreCase = false; // in the future this should be an uri-comparator
constructor(other?: ResourceMap<T>) {
this.map = other ? new Map(other.map) : new Map();
}
set(resource: URI, value: T): this {
@@ -550,20 +548,7 @@ export class ResourceMap<T> implements Map<URI, T> {
}
private toKey(resource: URI): string {
let key = resource.toString();
if (this.ignoreCase) {
key = key.toLowerCase();
}
return key;
}
clone(): ResourceMap<T> {
const resourceMap = new ResourceMap<T>();
this.map.forEach((value, key) => resourceMap.map.set(key, value));
return resourceMap;
return resource.toString();
}
}
@@ -580,18 +565,23 @@ export const enum Touch {
AsNew = 2
}
export class LinkedMap<K, V> {
export class LinkedMap<K, V> implements Map<K, V> {
readonly [Symbol.toStringTag] = 'LinkedMap';
private _map: Map<K, Item<K, V>>;
private _head: Item<K, V> | undefined;
private _tail: Item<K, V> | undefined;
private _size: number;
private _state: number;
constructor() {
this._map = new Map<K, Item<K, V>>();
this._head = undefined;
this._tail = undefined;
this._size = 0;
this._state = 0;
}
clear(): void {
@@ -599,6 +589,7 @@ export class LinkedMap<K, V> {
this._head = undefined;
this._tail = undefined;
this._size = 0;
this._state++;
}
isEmpty(): boolean {
@@ -632,7 +623,7 @@ export class LinkedMap<K, V> {
return item.value;
}
set(key: K, value: V, touch: Touch = Touch.None): void {
set(key: K, value: V, touch: Touch = Touch.None): this {
let item = this._map.get(key);
if (item) {
item.value = value;
@@ -658,6 +649,7 @@ export class LinkedMap<K, V> {
this._map.set(key, item);
this._size++;
}
return this;
}
delete(key: K): boolean {
@@ -690,6 +682,7 @@ export class LinkedMap<K, V> {
}
forEach(callbackfn: (value: V, key: K, map: LinkedMap<K, V>) => void, thisArg?: any): void {
const state = this._state;
let current = this._head;
while (current) {
if (thisArg) {
@@ -697,38 +690,25 @@ export class LinkedMap<K, V> {
} else {
callbackfn(current.value, current.key, this);
}
if (this._state !== state) {
throw new Error(`LinkedMap got modified during iteration.`);
}
current = current.next;
}
}
values(): V[] {
const result: V[] = [];
let current = this._head;
while (current) {
result.push(current.value);
current = current.next;
}
return result;
}
keys(): K[] {
const result: K[] = [];
let current = this._head;
while (current) {
result.push(current.key);
current = current.next;
}
return result;
}
/* VS Code / Monaco editor runs on es5 which has no Symbol.iterator
keys(): IterableIterator<K> {
const current = this._head;
const map = this;
const state = this._state;
let current = this._head;
const iterator: IterableIterator<K> = {
[Symbol.iterator]() {
return iterator;
},
next():IteratorResult<K> {
next(): IteratorResult<K> {
if (map._state !== state) {
throw new Error(`LinkedMap got modified during iteration.`);
}
if (current) {
const result = { value: current.key, done: false };
current = current.next;
@@ -742,12 +722,17 @@ export class LinkedMap<K, V> {
}
values(): IterableIterator<V> {
const current = this._head;
const map = this;
const state = this._state;
let current = this._head;
const iterator: IterableIterator<V> = {
[Symbol.iterator]() {
return iterator;
},
next():IteratorResult<V> {
next(): IteratorResult<V> {
if (map._state !== state) {
throw new Error(`LinkedMap got modified during iteration.`);
}
if (current) {
const result = { value: current.value, done: false };
current = current.next;
@@ -759,7 +744,34 @@ export class LinkedMap<K, V> {
};
return iterator;
}
*/
entries(): IterableIterator<[K, V]> {
const map = this;
const state = this._state;
let current = this._head;
const iterator: IterableIterator<[K, V]> = {
[Symbol.iterator]() {
return iterator;
},
next(): IteratorResult<[K, V]> {
if (map._state !== state) {
throw new Error(`LinkedMap got modified during iteration.`);
}
if (current) {
const result: IteratorResult<[K, V]> = { value: [current.key, current.value], done: false };
current = current.next;
return result;
} else {
return { value: undefined, done: true };
}
}
};
return iterator;
}
[Symbol.iterator](): IterableIterator<[K, V]> {
return this.entries();
}
protected trimOld(newSize: number) {
if (newSize >= this.size) {
@@ -781,6 +793,7 @@ export class LinkedMap<K, V> {
if (current) {
current.previous = undefined;
}
this._state++;
}
private addItemFirst(item: Item<K, V>): void {
@@ -794,6 +807,7 @@ export class LinkedMap<K, V> {
this._head.previous = item;
}
this._head = item;
this._state++;
}
private addItemLast(item: Item<K, V>): void {
@@ -807,6 +821,7 @@ export class LinkedMap<K, V> {
this._tail.next = item;
}
this._tail = item;
this._state++;
}
private removeItem(item: Item<K, V>): void {
@@ -843,6 +858,7 @@ export class LinkedMap<K, V> {
}
item.next = undefined;
item.previous = undefined;
this._state++;
}
private touch(item: Item<K, V>, touch: Touch): void {
@@ -879,6 +895,7 @@ export class LinkedMap<K, V> {
item.next = this._head;
this._head.previous = item;
this._head = item;
this._state++;
} else if (touch === Touch.AsNew) {
if (item === this._tail) {
return;
@@ -902,6 +919,7 @@ export class LinkedMap<K, V> {
item.previous = this._tail;
this._tail.next = item;
this._tail = item;
this._state++;
}
}
@@ -953,17 +971,18 @@ export class LRUCache<K, V> extends LinkedMap<K, V> {
this.checkTrim();
}
get(key: K): V | undefined {
return super.get(key, Touch.AsNew);
get(key: K, touch: Touch = Touch.AsNew): V | undefined {
return super.get(key, touch);
}
peek(key: K): V | undefined {
return super.get(key, Touch.None);
}
set(key: K, value: V): void {
set(key: K, value: V): this {
super.set(key, value, Touch.AsNew);
this.checkTrim();
return this;
}
private checkTrim() {

View File

@@ -335,3 +335,13 @@ export function getMediaMime(path: string): string | undefined {
const ext = extname(path);
return mapExtToMediaMimes[ext.toLowerCase()];
}
export function getExtensionForMimeType(mimeType: string): string | undefined {
for (const extension in mapExtToMediaMimes) {
if (mapExtToMediaMimes[extension] === mimeType) {
return extension;
}
}
return undefined;
}

View File

@@ -59,6 +59,8 @@ export namespace Schemas {
export const vscodeSettings = 'vscode-settings';
export const webviewPanel = 'webview-panel';
export const vscodeWebviewResource = 'vscode-webview-resource';
}
class RemoteAuthoritiesImpl {

View File

@@ -6,7 +6,7 @@
import * as extpath from 'vs/base/common/extpath';
import * as paths from 'vs/base/common/path';
import { URI, uriToFsPath } from 'vs/base/common/uri';
import { equalsIgnoreCase } from 'vs/base/common/strings';
import { equalsIgnoreCase, compare as strCompare, compareIgnoreCase } from 'vs/base/common/strings';
import { Schemas } from 'vs/base/common/network';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { CharCode } from 'vs/base/common/charCode';
@@ -17,248 +17,363 @@ export function originalFSPath(uri: URI): string {
return uriToFsPath(uri, true);
}
/**
* Creates a key from a resource URI to be used to resource comparison and for resource maps.
* URI queries are included, fragments are ignored.
*/
export function getComparisonKey(resource: URI, caseInsensitivePath = hasToIgnoreCase(resource)): string {
let path = resource.path || '/';
if (caseInsensitivePath) {
path = path.toLowerCase();
}
return resource.with({ authority: resource.authority.toLowerCase(), path: path, fragment: null }).toString();
//#region IExtUri
export interface IExtUri {
// --- identity
/**
* Compares two uris.
*
* @param uri1 Uri
* @param uri2 Uri
* @param ignoreFragment Ignore the fragment (defaults to `false`)
*/
compare(uri1: URI, uri2: URI, ignoreFragment?: boolean): number;
/**
* Tests whether two uris are equal
*
* @param uri1 Uri
* @param uri2 Uri
* @param ignoreFragment Ignore the fragment (defaults to `false`)
*/
isEqual(uri1: URI | undefined, uri2: URI | undefined, ignoreFragment?: boolean): boolean;
/**
* Tests whether a `candidate` URI is a parent or equal of a given `base` URI.
*
* @param base A uri which is "longer"
* @param parentCandidate A uri which is "shorter" then `base`
* @param ignoreFragment Ignore the fragment (defaults to `false`)
*/
isEqualOrParent(base: URI, parentCandidate: URI, ignoreFragment?: boolean): boolean;
/**
* Creates a key from a resource URI to be used to resource comparison and for resource maps.
* @see ResourceMap
* @param uri Uri
* @param ignoreFragment Ignore the fragment (defaults to `false`)
*/
getComparisonKey(uri: URI, ignoreFragment?: boolean): string;
// --- path math
basenameOrAuthority(resource: URI): string;
/**
* Returns the basename of the path component of an uri.
* @param resource
*/
basename(resource: URI): string;
/**
* Returns the extension of the path component of an uri.
* @param resource
*/
extname(resource: URI): string;
/**
* Return a URI representing the directory of a URI path.
*
* @param resource The input URI.
* @returns The URI representing the directory of the input URI.
*/
dirname(resource: URI): URI;
/**
* Join a URI path with path fragments and normalizes the resulting path.
*
* @param resource The input URI.
* @param pathFragment The path fragment to add to the URI path.
* @returns The resulting URI.
*/
joinPath(resource: URI, ...pathFragment: string[]): URI
/**
* Normalizes the path part of a URI: Resolves `.` and `..` elements with directory names.
*
* @param resource The URI to normalize the path.
* @returns The URI with the normalized path.
*/
normalizePath(resource: URI): URI;
/**
*
* @param from
* @param to
*/
relativePath(from: URI, to: URI): string | undefined;
/**
* Resolves an absolute or relative path against a base URI.
* The path can be relative or absolute posix or a Windows path
*/
resolvePath(base: URI, path: string): URI;
// --- misc
/**
* Returns true if the URI path is absolute.
*/
isAbsolutePath(resource: URI): boolean;
/**
* Tests whether the two authorities are the same
*/
isEqualAuthority(a1: string, a2: string): boolean;
/**
* Returns true if the URI path has a trailing path separator
*/
hasTrailingPathSeparator(resource: URI, sep?: string): boolean;
/**
* Removes a trailing path separator, if there's one.
* Important: Doesn't remove the first slash, it would make the URI invalid
*/
removeTrailingPathSeparator(resource: URI, sep?: string): URI;
/**
* Adds a trailing path separator to the URI if there isn't one already.
* For example, c:\ would be unchanged, but c:\users would become c:\users\
*/
addTrailingPathSeparator(resource: URI, sep?: string): URI;
}
export function hasToIgnoreCase(resource: URI | undefined): boolean {
// A file scheme resource is in the same platform as code, so ignore case for non linux platforms
// Resource can be from another platform. Lowering the case as an hack. Should come from File system provider
return resource && resource.scheme === Schemas.file ? !isLinux : true;
}
export class ExtUri implements IExtUri {
export function basenameOrAuthority(resource: URI): string {
return basename(resource) || resource.authority;
}
constructor(private _ignorePathCasing: (uri: URI) => boolean) { }
/**
* Tests whether a `candidate` URI is a parent or equal of a given `base` URI.
* URI queries must match, fragments are ignored.
* @param base A uri which is "longer"
* @param parentCandidate A uri which is "shorter" then `base`
*/
export function isEqualOrParent(base: URI, parentCandidate: URI, ignoreCase = hasToIgnoreCase(base)): boolean {
if (base.scheme === parentCandidate.scheme) {
if (base.scheme === Schemas.file) {
return extpath.isEqualOrParent(originalFSPath(base), originalFSPath(parentCandidate), ignoreCase) && base.query === parentCandidate.query;
}
if (isEqualAuthority(base.authority, parentCandidate.authority)) {
return extpath.isEqualOrParent(base.path || '/', parentCandidate.path || '/', ignoreCase, '/') && base.query === parentCandidate.query;
}
}
return false;
}
/**
* Tests whether the two authorities are the same
*/
export function isEqualAuthority(a1: string, a2: string) {
return a1 === a2 || equalsIgnoreCase(a1, a2);
}
/**
* Tests whether two resources are the same. URI queries must match, fragments are ignored unless requested.
*/
export function isEqual(first: URI | undefined, second: URI | undefined, caseInsensitivePath = hasToIgnoreCase(first), ignoreFragment = true): boolean {
if (first === second) {
return true;
}
if (!first || !second) {
return false;
}
if (first.scheme !== second.scheme || !isEqualAuthority(first.authority, second.authority)) {
return false;
}
const p1 = first.path || '/', p2 = second.path || '/';
return (p1 === p2 || caseInsensitivePath && equalsIgnoreCase(p1, p2)) && first.query === second.query && (ignoreFragment || first.fragment === second.fragment);
}
export function basename(resource: URI): string {
return paths.posix.basename(resource.path);
}
export function extname(resource: URI): string {
return paths.posix.extname(resource.path);
}
/**
* Return a URI representing the directory of a URI path.
*
* @param resource The input URI.
* @returns The URI representing the directory of the input URI.
*/
export function dirname(resource: URI): URI {
if (resource.path.length === 0) {
return resource;
}
let dirname;
if (resource.scheme === Schemas.file) {
dirname = URI.file(paths.dirname(originalFSPath(resource))).path;
} else {
dirname = paths.posix.dirname(resource.path);
if (resource.authority && dirname.length && dirname.charCodeAt(0) !== CharCode.Slash) {
console.error(`dirname("${resource.toString})) resulted in a relative path`);
dirname = '/'; // If a URI contains an authority component, then the path component must either be empty or begin with a CharCode.Slash ("/") character
}
}
return resource.with({
path: dirname
});
}
/**
* Join a URI path with path fragments and normalizes the resulting path.
*
* @param resource The input URI.
* @param pathFragment The path fragment to add to the URI path.
* @returns The resulting URI.
*/
export function joinPath(resource: URI, ...pathFragment: string[]): URI {
let joinedPath: string;
if (resource.scheme === 'file') {
joinedPath = URI.file(paths.join(originalFSPath(resource), ...pathFragment)).path;
} else {
joinedPath = paths.posix.join(resource.path || '/', ...pathFragment);
}
return resource.with({
path: joinedPath
});
}
/**
* Normalizes the path part of a URI: Resolves `.` and `..` elements with directory names.
*
* @param resource The URI to normalize the path.
* @returns The URI with the normalized path.
*/
export function normalizePath(resource: URI): URI {
if (!resource.path.length) {
return resource;
}
let normalizedPath: string;
if (resource.scheme === Schemas.file) {
normalizedPath = URI.file(paths.normalize(originalFSPath(resource))).path;
} else {
normalizedPath = paths.posix.normalize(resource.path);
}
return resource.with({
path: normalizedPath
});
}
/**
* Returns true if the URI path is absolute.
*/
export function isAbsolutePath(resource: URI): boolean {
return !!resource.path && resource.path[0] === '/';
}
/**
* Returns true if the URI path has a trailing path separator
*/
export function hasTrailingPathSeparator(resource: URI, sep: string = paths.sep): boolean {
if (resource.scheme === Schemas.file) {
const fsp = originalFSPath(resource);
return fsp.length > extpath.getRoot(fsp).length && fsp[fsp.length - 1] === sep;
} else {
const p = resource.path;
return (p.length > 1 && p.charCodeAt(p.length - 1) === CharCode.Slash) && !(/^[a-zA-Z]:(\/$|\\$)/.test(resource.fsPath)); // ignore the slash at offset 0
}
}
/**
* Removes a trailing path separator, if there's one.
* Important: Doesn't remove the first slash, it would make the URI invalid
*/
export function removeTrailingPathSeparator(resource: URI, sep: string = paths.sep): URI {
// Make sure that the path isn't a drive letter. A trailing separator there is not removable.
if (hasTrailingPathSeparator(resource, sep)) {
return resource.with({ path: resource.path.substr(0, resource.path.length - 1) });
}
return resource;
}
/**
* Adds a trailing path separator to the URI if there isn't one already.
* For example, c:\ would be unchanged, but c:\users would become c:\users\
*/
export function addTrailingPathSeparator(resource: URI, sep: string = paths.sep): URI {
let isRootSep: boolean = false;
if (resource.scheme === Schemas.file) {
const fsp = originalFSPath(resource);
isRootSep = ((fsp !== undefined) && (fsp.length === extpath.getRoot(fsp).length) && (fsp[fsp.length - 1] === sep));
} else {
sep = '/';
const p = resource.path;
isRootSep = p.length === 1 && p.charCodeAt(p.length - 1) === CharCode.Slash;
}
if (!isRootSep && !hasTrailingPathSeparator(resource, sep)) {
return resource.with({ path: resource.path + '/' });
}
return resource;
}
/**
* Returns a relative path between two URIs. If the URIs don't have the same schema or authority, `undefined` is returned.
* The returned relative path always uses forward slashes.
*/
export function relativePath(from: URI, to: URI, caseInsensitivePath = hasToIgnoreCase(from)): string | undefined {
if (from.scheme !== to.scheme || !isEqualAuthority(from.authority, to.authority)) {
return undefined;
}
if (from.scheme === Schemas.file) {
const relativePath = paths.relative(originalFSPath(from), originalFSPath(to));
return isWindows ? extpath.toSlashes(relativePath) : relativePath;
}
let fromPath = from.path || '/', toPath = to.path || '/';
if (caseInsensitivePath) {
// make casing of fromPath match toPath
let i = 0;
for (const len = Math.min(fromPath.length, toPath.length); i < len; i++) {
if (fromPath.charCodeAt(i) !== toPath.charCodeAt(i)) {
if (fromPath.charAt(i).toLowerCase() !== toPath.charAt(i).toLowerCase()) {
break;
compare(uri1: URI, uri2: URI, ignoreFragment: boolean = false): number {
// scheme
let ret = strCompare(uri1.scheme, uri2.scheme);
if (ret === 0) {
// authority
ret = compareIgnoreCase(uri1.authority, uri2.authority);
if (ret === 0) {
// path
ret = this._ignorePathCasing(uri1) ? compareIgnoreCase(uri1.path, uri2.path) : strCompare(uri1.path, uri2.path);
// query
if (ret === 0) {
ret = strCompare(uri1.query, uri2.query);
// fragment
if (ret === 0 && !ignoreFragment) {
ret = strCompare(uri1.fragment, uri2.fragment);
}
}
}
}
fromPath = toPath.substr(0, i) + fromPath.substr(i);
return ret;
}
getComparisonKey(uri: URI, ignoreFragment: boolean = false): string {
return uri.with({
path: this._ignorePathCasing(uri) ? uri.path.toLowerCase() : undefined,
fragment: ignoreFragment ? null : undefined
}).toString();
}
isEqual(uri1: URI | undefined, uri2: URI | undefined, ignoreFragment: boolean = false): boolean {
if (uri1 === uri2) {
return true;
}
if (!uri1 || !uri2) {
return false;
}
if (uri1.scheme !== uri2.scheme || !isEqualAuthority(uri1.authority, uri2.authority)) {
return false;
}
const p1 = uri1.path, p2 = uri2.path;
return (p1 === p2 || this._ignorePathCasing(uri1) && equalsIgnoreCase(p1, p2)) && uri1.query === uri2.query && (ignoreFragment || uri1.fragment === uri2.fragment);
}
isEqualOrParent(base: URI, parentCandidate: URI, ignoreFragment: boolean = false): boolean {
if (base.scheme === parentCandidate.scheme) {
if (base.scheme === Schemas.file) {
return extpath.isEqualOrParent(originalFSPath(base), originalFSPath(parentCandidate), this._ignorePathCasing(base)) && base.query === parentCandidate.query && (ignoreFragment || base.fragment === parentCandidate.fragment);
}
if (isEqualAuthority(base.authority, parentCandidate.authority)) {
return extpath.isEqualOrParent(base.path, parentCandidate.path, this._ignorePathCasing(base), '/') && base.query === parentCandidate.query && (ignoreFragment || base.fragment === parentCandidate.fragment);
}
}
return false;
}
// --- path math
joinPath(resource: URI, ...pathFragment: string[]): URI {
return URI.joinPath(resource, ...pathFragment);
}
basenameOrAuthority(resource: URI): string {
return basename(resource) || resource.authority;
}
basename(resource: URI): string {
return paths.posix.basename(resource.path);
}
extname(resource: URI): string {
return paths.posix.extname(resource.path);
}
dirname(resource: URI): URI {
if (resource.path.length === 0) {
return resource;
}
let dirname;
if (resource.scheme === Schemas.file) {
dirname = URI.file(paths.dirname(originalFSPath(resource))).path;
} else {
dirname = paths.posix.dirname(resource.path);
if (resource.authority && dirname.length && dirname.charCodeAt(0) !== CharCode.Slash) {
console.error(`dirname("${resource.toString})) resulted in a relative path`);
dirname = '/'; // If a URI contains an authority component, then the path component must either be empty or begin with a CharCode.Slash ("/") character
}
}
return resource.with({
path: dirname
});
}
normalizePath(resource: URI): URI {
if (!resource.path.length) {
return resource;
}
let normalizedPath: string;
if (resource.scheme === Schemas.file) {
normalizedPath = URI.file(paths.normalize(originalFSPath(resource))).path;
} else {
normalizedPath = paths.posix.normalize(resource.path);
}
return resource.with({
path: normalizedPath
});
}
relativePath(from: URI, to: URI): string | undefined {
if (from.scheme !== to.scheme || !isEqualAuthority(from.authority, to.authority)) {
return undefined;
}
if (from.scheme === Schemas.file) {
const relativePath = paths.relative(originalFSPath(from), originalFSPath(to));
return isWindows ? extpath.toSlashes(relativePath) : relativePath;
}
let fromPath = from.path || '/', toPath = to.path || '/';
if (this._ignorePathCasing(from)) {
// make casing of fromPath match toPath
let i = 0;
for (const len = Math.min(fromPath.length, toPath.length); i < len; i++) {
if (fromPath.charCodeAt(i) !== toPath.charCodeAt(i)) {
if (fromPath.charAt(i).toLowerCase() !== toPath.charAt(i).toLowerCase()) {
break;
}
}
}
fromPath = toPath.substr(0, i) + fromPath.substr(i);
}
return paths.posix.relative(fromPath, toPath);
}
resolvePath(base: URI, path: string): URI {
if (base.scheme === Schemas.file) {
const newURI = URI.file(paths.resolve(originalFSPath(base), path));
return base.with({
authority: newURI.authority,
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;
}
}
return base.with({
path: paths.posix.resolve(base.path, path)
});
}
// --- misc
isAbsolutePath(resource: URI): boolean {
return !!resource.path && resource.path[0] === '/';
}
isEqualAuthority(a1: string, a2: string) {
return a1 === a2 || equalsIgnoreCase(a1, a2);
}
hasTrailingPathSeparator(resource: URI, sep: string = paths.sep): boolean {
if (resource.scheme === Schemas.file) {
const fsp = originalFSPath(resource);
return fsp.length > extpath.getRoot(fsp).length && fsp[fsp.length - 1] === sep;
} else {
const p = resource.path;
return (p.length > 1 && p.charCodeAt(p.length - 1) === CharCode.Slash) && !(/^[a-zA-Z]:(\/$|\\$)/.test(resource.fsPath)); // ignore the slash at offset 0
}
}
removeTrailingPathSeparator(resource: URI, sep: string = paths.sep): URI {
// Make sure that the path isn't a drive letter. A trailing separator there is not removable.
if (hasTrailingPathSeparator(resource, sep)) {
return resource.with({ path: resource.path.substr(0, resource.path.length - 1) });
}
return resource;
}
addTrailingPathSeparator(resource: URI, sep: string = paths.sep): URI {
let isRootSep: boolean = false;
if (resource.scheme === Schemas.file) {
const fsp = originalFSPath(resource);
isRootSep = ((fsp !== undefined) && (fsp.length === extpath.getRoot(fsp).length) && (fsp[fsp.length - 1] === sep));
} else {
sep = '/';
const p = resource.path;
isRootSep = p.length === 1 && p.charCodeAt(p.length - 1) === CharCode.Slash;
}
if (!isRootSep && !hasTrailingPathSeparator(resource, sep)) {
return resource.with({ path: resource.path + '/' });
}
return resource;
}
return paths.posix.relative(fromPath, toPath);
}
/**
* Resolves an absolute or relative path against a base URI.
* The path can be relative or absolute posix or a Windows path
* Unbiased utility that takes uris "as they are". This means it can be interchanged with
* uri#toString() usages. The following is true
* ```
* assertEqual(aUri.toString() === bUri.toString(), exturi.isEqual(aUri, bUri))
* ```
*/
export function resolvePath(base: URI, path: string): URI {
if (base.scheme === Schemas.file) {
const newURI = URI.file(paths.resolve(originalFSPath(base), path));
return base.with({
authority: newURI.authority,
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;
}
}
return base.with({
path: paths.posix.resolve(base.path, path)
});
}
export const extUri = new ExtUri(() => false);
/**
* BIASED utility that always ignores the casing of uris path. ONLY use these util if you
* understand what you are doing.
*
* Note that `IUriIdentityService#extUri` is a better replacement for this because that utility
* knows when path casing matters and when not.
*/
export const extUriIgnorePathCase = new ExtUri(_ => true);
const exturiBiasedIgnorePathCase = new ExtUri(uri => {
// A file scheme resource is in the same platform as code, so ignore case for non linux platforms
// Resource can be from another platform. Lowering the case as an hack. Should come from File system provider
return uri && uri.scheme === Schemas.file ? !isLinux : true;
});
export const isEqual = exturiBiasedIgnorePathCase.isEqual.bind(exturiBiasedIgnorePathCase);
export const isEqualOrParent = exturiBiasedIgnorePathCase.isEqualOrParent.bind(exturiBiasedIgnorePathCase);
export const getComparisonKey = exturiBiasedIgnorePathCase.getComparisonKey.bind(exturiBiasedIgnorePathCase);
export const basenameOrAuthority = exturiBiasedIgnorePathCase.basenameOrAuthority.bind(exturiBiasedIgnorePathCase);
export const basename = exturiBiasedIgnorePathCase.basename.bind(exturiBiasedIgnorePathCase);
export const extname = exturiBiasedIgnorePathCase.extname.bind(exturiBiasedIgnorePathCase);
export const dirname = exturiBiasedIgnorePathCase.dirname.bind(exturiBiasedIgnorePathCase);
export const joinPath = extUri.joinPath.bind(extUri);
export const normalizePath = exturiBiasedIgnorePathCase.normalizePath.bind(exturiBiasedIgnorePathCase);
export const relativePath = exturiBiasedIgnorePathCase.relativePath.bind(exturiBiasedIgnorePathCase);
export const resolvePath = exturiBiasedIgnorePathCase.resolvePath.bind(exturiBiasedIgnorePathCase);
export const isAbsolutePath = exturiBiasedIgnorePathCase.isAbsolutePath.bind(exturiBiasedIgnorePathCase);
export const isEqualAuthority = exturiBiasedIgnorePathCase.isEqualAuthority.bind(exturiBiasedIgnorePathCase);
export const hasTrailingPathSeparator = exturiBiasedIgnorePathCase.hasTrailingPathSeparator.bind(exturiBiasedIgnorePathCase);
export const removeTrailingPathSeparator = exturiBiasedIgnorePathCase.removeTrailingPathSeparator.bind(exturiBiasedIgnorePathCase);
export const addTrailingPathSeparator = exturiBiasedIgnorePathCase.addTrailingPathSeparator.bind(exturiBiasedIgnorePathCase);
//#endregion
export function distinctParents<T>(items: T[], resourceAccessor: (item: T) => URI): T[] {
const distinctParents: T[] = [];

View File

@@ -13,10 +13,18 @@ export const enum ScrollbarVisibility {
}
export interface ScrollEvent {
oldWidth: number;
oldScrollWidth: number;
oldScrollLeft: number;
width: number;
scrollWidth: number;
scrollLeft: number;
oldHeight: number;
oldScrollHeight: number;
oldScrollTop: number;
height: number;
scrollHeight: number;
scrollTop: number;
@@ -134,10 +142,18 @@ export class ScrollState implements IScrollDimensions, IScrollPosition {
const scrollTopChanged = (this.scrollTop !== previous.scrollTop);
return {
oldWidth: previous.width,
oldScrollWidth: previous.scrollWidth,
oldScrollLeft: previous.scrollLeft,
width: this.width,
scrollWidth: this.scrollWidth,
scrollLeft: this.scrollLeft,
oldHeight: previous.height,
oldScrollHeight: previous.scrollHeight,
oldScrollTop: previous.scrollTop,
height: this.height,
scrollHeight: this.scrollHeight,
scrollTop: this.scrollTop,

View File

@@ -0,0 +1,203 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class Node<K, V> {
readonly forward: Node<K, V>[];
constructor(readonly level: number, readonly key: K, public value: V) {
this.forward = [];
}
}
const NIL: undefined = undefined;
interface Comparator<K> {
(a: K, b: K): number;
}
export class SkipList<K, V> implements Map<K, V> {
readonly [Symbol.toStringTag] = 'SkipList';
private _maxLevel: number;
private _level: number = 1;
private _header: Node<K, V>;
private _size: number = 0;
/**
*
* @param capacity Capacity at which the list performs best
*/
constructor(
readonly comparator: (a: K, b: K) => number,
capacity: number = 2 ** 16
) {
this._maxLevel = Math.max(1, Math.log2(capacity) | 0);
this._header = <any>new Node(this._maxLevel, NIL, NIL);
}
get size(): number {
return this._size;
}
clear(): void {
this._header = <any>new Node(this._maxLevel, NIL, NIL);
}
has(key: K): boolean {
return Boolean(SkipList._search(this, key, this.comparator));
}
get(key: K): V | undefined {
return SkipList._search(this, key, this.comparator)?.value;
}
set(key: K, value: V): this {
if (SkipList._insert(this, key, value, this.comparator)) {
this._size += 1;
}
return this;
}
delete(key: K): boolean {
const didDelete = SkipList._delete(this, key, this.comparator);
if (didDelete) {
this._size -= 1;
}
return didDelete;
}
// --- iteration
forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void {
let node = this._header.forward[0];
while (node) {
callbackfn.call(thisArg, node.value, node.key, this);
node = node.forward[0];
}
}
[Symbol.iterator](): IterableIterator<[K, V]> {
return this.entries();
}
*entries(): IterableIterator<[K, V]> {
let node = this._header.forward[0];
while (node) {
yield [node.key, node.value];
node = node.forward[0];
}
}
*keys(): IterableIterator<K> {
let node = this._header.forward[0];
while (node) {
yield node.key;
node = node.forward[0];
}
}
*values(): IterableIterator<V> {
let node = this._header.forward[0];
while (node) {
yield node.value;
node = node.forward[0];
}
}
toString(): string {
// debug string...
let result = '[SkipList]:';
let node = this._header.forward[0];
while (node) {
result += `node(${node.key}, ${node.value}, lvl:${node.level})`;
node = node.forward[0];
}
return result;
}
// from https://www.epaperpress.com/sortsearch/download/skiplist.pdf
private static _search<K, V>(list: SkipList<K, V>, searchKey: K, comparator: Comparator<K>) {
let x = list._header;
for (let i = list._level; i >= 0; i--) {
while (x.forward[i] && comparator(x.forward[i].key, searchKey) < 0) {
x = x.forward[i];
}
}
x = x.forward[0];
if (x && comparator(x.key, searchKey) === 0) {
return x;
}
return undefined;
}
private static _insert<K, V>(list: SkipList<K, V>, searchKey: K, value: V, comparator: Comparator<K>) {
let update: Node<K, V>[] = [];
let x = list._header;
for (let i = list._level; i >= 0; i--) {
while (x.forward[i] && comparator(x.forward[i].key, searchKey) < 0) {
x = x.forward[i];
}
update[i] = x;
}
x = x.forward[0];
if (x && comparator(x.key, searchKey) === 0) {
// update
x.value = value;
return false;
} else {
// insert
let lvl = SkipList._randomLevel(list);
if (lvl > list._level) {
for (let i = list._level + 1; i <= lvl; i++) {
update[i] = list._header;
}
list._level = lvl;
}
x = new Node<K, V>(lvl, searchKey, value);
for (let i = 0; i <= lvl; i++) {
x.forward[i] = update[i].forward[i];
update[i].forward[i] = x;
}
return true;
}
}
private static _randomLevel(list: SkipList<any, any>, p: number = 0.5): number {
let lvl = 1;
while (Math.random() < p && lvl < list._maxLevel) {
lvl += 1;
}
return lvl;
}
private static _delete<K, V>(list: SkipList<K, V>, searchKey: K, comparator: Comparator<K>) {
let update: Node<K, V>[] = [];
let x = list._header;
for (let i = list._level; i >= 0; i--) {
while (x.forward[i] && comparator(x.forward[i].key, searchKey) < 0) {
x = x.forward[i];
}
update[i] = x;
}
x = x.forward[0];
if (!x || comparator(x.key, searchKey) !== 0) {
// not found
return false;
}
for (let i = 0; i < list._level; i++) {
if (update[i].forward[i] !== x) {
break;
}
update[i].forward[i] = x.forward[i];
}
while (list._level >= 1 && list._header.forward[list._level] === NIL) {
list._level -= 1;
}
return true;
}
}

View File

@@ -728,6 +728,14 @@ export function isBasicASCII(str: string): boolean {
return IS_BASIC_ASCII.test(str);
}
export const UNUSUAL_LINE_TERMINATORS = /[\u2028\u2029\u0085]/; // LINE SEPARATOR (LS), PARAGRAPH SEPARATOR (PS), NEXT LINE (NEL)
/**
* Returns true if `str` contains unusual line terminators, like LS, PS or NEL
*/
export function containsUnusualLineTerminators(str: string): boolean {
return UNUSUAL_LINE_TERMINATORS.test(str);
}
export function containsFullWidthCharacter(str: string): boolean {
for (let i = 0, len = str.length; i < len; i++) {
if (isFullWidthCharacter(str.charCodeAt(i))) {

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ipcRenderer, Event } from 'electron';
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
import { IContextMenuItem, ISerializableContextMenuItem, CONTEXT_MENU_CLOSE_CHANNEL, CONTEXT_MENU_CHANNEL, IPopupOptions, IContextMenuEvent } from 'vs/base/parts/contextmenu/common/contextmenu';
let contextMenuIdPool = 0;
@@ -13,7 +13,7 @@ export function popup(items: IContextMenuItem[], options?: IPopupOptions): void
const contextMenuId = contextMenuIdPool++;
const onClickChannel = `vscode:onContextMenu${contextMenuId}`;
const onClickChannelHandler = (_event: Event, itemId: number, context: IContextMenuEvent) => {
const onClickChannelHandler = (event: unknown, itemId: number, context: IContextMenuEvent) => {
const item = processedItems[itemId];
if (item.click) {
item.click(context);
@@ -21,7 +21,7 @@ export function popup(items: IContextMenuItem[], options?: IPopupOptions): void
};
ipcRenderer.once(onClickChannel, onClickChannelHandler);
ipcRenderer.once(CONTEXT_MENU_CLOSE_CHANNEL, (_event: Event, closedContextMenuId: number) => {
ipcRenderer.once(CONTEXT_MENU_CLOSE_CHANNEL, (event: unknown, closedContextMenuId: number) => {
if (closedContextMenuId !== contextMenuId) {
return;
}

View File

@@ -8,7 +8,7 @@ import { Event } from 'vs/base/common/event';
import { VSBuffer } from 'vs/base/common/buffer';
export interface Sender {
send(channel: string, msg: Buffer | null): void;
send(channel: string, msg: unknown): void;
}
export class Protocol implements IMessagePassingProtocol {
@@ -17,13 +17,13 @@ export class Protocol implements IMessagePassingProtocol {
send(message: VSBuffer): void {
try {
this.sender.send('ipc:message', (<Buffer>message.buffer));
this.sender.send('vscode:message', message.buffer);
} catch (e) {
// systems are going down
}
}
dispose(): void {
this.sender.send('ipc:disconnect', null);
this.sender.send('vscode:disconnect', null);
}
}
}

View File

@@ -10,7 +10,9 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance
import * as errors from 'vs/base/common/errors';
import { VSBuffer } from 'vs/base/common/buffer';
import { getRandomElement } from 'vs/base/common/arrays';
import { isFunction } from 'vs/base/common/types';
import { isFunction, isUndefinedOrNull } from 'vs/base/common/types';
import { revive } from 'vs/base/common/marshalling';
import { isUpperAsciiLetter } from 'vs/base/common/strings';
/**
* An `IChannel` is an abstraction over a collection of commands.
@@ -919,3 +921,142 @@ export class StaticRouter<TContext = string> implements IClientRouter<TContext>
return await this.route(hub);
}
}
//#region createChannelReceiver / createChannelSender
/**
* Use both `createChannelReceiver` and `createChannelSender`
* for automated process <=> process communication over methods
* and events. You do not need to spell out each method on both
* sides, a proxy will take care of this.
*
* Rules:
* - if marshalling is enabled, only `URI` and `RegExp` is converted
* automatically for you
* - events must follow the naming convention `onUppercase`
* - `CancellationToken` is currently not supported
* - if a context is provided, you can use `AddFirstParameterToFunctions`
* utility to signal this in the receiving side type
*/
export interface IBaseChannelOptions {
/**
* Disables automatic marshalling of `URI`.
* If marshalling is disabled, `UriComponents`
* must be used instead.
*/
disableMarshalling?: boolean;
}
export interface IChannelReceiverOptions extends IBaseChannelOptions { }
export function createChannelReceiver(service: unknown, options?: IChannelReceiverOptions): IServerChannel {
const handler = service as { [key: string]: unknown };
const disableMarshalling = options && options.disableMarshalling;
// Buffer any event that should be supported by
// iterating over all property keys and finding them
const mapEventNameToEvent = new Map<string, Event<unknown>>();
for (const key in handler) {
if (propertyIsEvent(key)) {
mapEventNameToEvent.set(key, Event.buffer(handler[key] as Event<unknown>, true));
}
}
return new class implements IServerChannel {
listen<T>(_: unknown, event: string): Event<T> {
const eventImpl = mapEventNameToEvent.get(event);
if (eventImpl) {
return eventImpl as Event<T>;
}
throw new Error(`Event not found: ${event}`);
}
call(_: unknown, command: string, args?: any[]): Promise<any> {
const target = handler[command];
if (typeof target === 'function') {
// Revive unless marshalling disabled
if (!disableMarshalling && Array.isArray(args)) {
for (let i = 0; i < args.length; i++) {
args[i] = revive(args[i]);
}
}
return target.apply(handler, args);
}
throw new Error(`Method not found: ${command}`);
}
};
}
export interface IChannelSenderOptions extends IBaseChannelOptions {
/**
* If provided, will add the value of `context`
* to each method call to the target.
*/
context?: unknown;
/**
* If provided, will not proxy any of the properties
* that are part of the Map but rather return that value.
*/
properties?: Map<string, unknown>;
}
export function createChannelSender<T>(channel: IChannel, options?: IChannelSenderOptions): T {
const disableMarshalling = options && options.disableMarshalling;
return new Proxy({}, {
get(_target: T, propKey: PropertyKey) {
if (typeof propKey === 'string') {
// Check for predefined values
if (options?.properties?.has(propKey)) {
return options.properties.get(propKey);
}
// Event
if (propertyIsEvent(propKey)) {
return channel.listen(propKey);
}
// Function
return async function (...args: any[]) {
// Add context if any
let methodArgs: any[];
if (options && !isUndefinedOrNull(options.context)) {
methodArgs = [options.context, ...args];
} else {
methodArgs = args;
}
const result = await channel.call(propKey, methodArgs);
// Revive unless marshalling disabled
if (!disableMarshalling) {
return revive(result);
}
return result;
};
}
throw new Error(`Property not found: ${String(propKey)}`);
}
}) as T;
}
function propertyIsEvent(name: string): boolean {
// Assume a property is an event if it has a form of "onSomething"
return name[0] === 'o' && name[1] === 'n' && isUpperAsciiLetter(name.charCodeAt(2));
}
//#endregion

View File

@@ -5,7 +5,7 @@
import { Event, Emitter } from 'vs/base/common/event';
import { IPCServer, ClientConnectionEvent } from 'vs/base/parts/ipc/common/ipc';
import { Protocol } from 'vs/base/parts/ipc/node/ipc.electron';
import { Protocol } from 'vs/base/parts/ipc/common/ipc.electron';
import { ipcMain, WebContents } from 'electron';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { VSBuffer } from 'vs/base/common/buffer';
@@ -27,7 +27,7 @@ export class Server extends IPCServer {
private static readonly Clients = new Map<number, IDisposable>();
private static getOnDidClientConnect(): Event<ClientConnectionEvent> {
const onHello = Event.fromNodeEventEmitter<WebContents>(ipcMain, 'ipc:hello', ({ sender }) => sender);
const onHello = Event.fromNodeEventEmitter<WebContents>(ipcMain, 'vscode:hello', ({ sender }) => sender);
return Event.map(onHello, webContents => {
const id = webContents.id;
@@ -40,8 +40,8 @@ export class Server extends IPCServer {
const onDidClientReconnect = new Emitter<void>();
Server.Clients.set(id, toDisposable(() => onDidClientReconnect.fire()));
const onMessage = createScopedOnMessageEvent(id, 'ipc:message') as Event<VSBuffer>;
const onDidClientDisconnect = Event.any(Event.signal(createScopedOnMessageEvent(id, 'ipc:disconnect')), onDidClientReconnect.event);
const onMessage = createScopedOnMessageEvent(id, 'vscode:message') as Event<VSBuffer>;
const onDidClientDisconnect = Event.any(Event.signal(createScopedOnMessageEvent(id, 'vscode:disconnect')), onDidClientReconnect.event);
const protocol = new Protocol(webContents, onMessage);
return { protocol, onDidClientDisconnect };

View File

@@ -5,18 +5,18 @@
import { Event } from 'vs/base/common/event';
import { IPCClient } from 'vs/base/parts/ipc/common/ipc';
import { Protocol } from 'vs/base/parts/ipc/node/ipc.electron';
import { ipcRenderer } from 'electron';
import { Protocol } from 'vs/base/parts/ipc/common/ipc.electron';
import { IDisposable } from 'vs/base/common/lifecycle';
import { VSBuffer } from 'vs/base/common/buffer';
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
export class Client extends IPCClient implements IDisposable {
private protocol: Protocol;
private static createProtocol(): Protocol {
const onMessage = Event.fromNodeEventEmitter<VSBuffer>(ipcRenderer, 'ipc:message', (_, message: Buffer) => VSBuffer.wrap(message));
ipcRenderer.send('ipc:hello');
const onMessage = Event.fromNodeEventEmitter<VSBuffer>(ipcRenderer, 'vscode:message', (_, message) => VSBuffer.wrap(message));
ipcRenderer.send('vscode:hello');
return new Protocol(ipcRenderer, onMessage);
}
@@ -29,4 +29,4 @@ export class Client extends IPCClient implements IDisposable {
dispose(): void {
this.protocol.dispose();
}
}
}

View File

@@ -1,133 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc';
import { revive } from 'vs/base/common/marshalling';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { isUpperAsciiLetter } from 'vs/base/common/strings';
/**
* Use both `createChannelReceiver` and `createChannelSender`
* for automated process <=> process communication over methods
* and events. You do not need to spell out each method on both
* sides, a proxy will take care of this.
*
* Rules:
* - if marshalling is enabled, only `URI` and `RegExp` is converted
* automatically for you
* - events must follow the naming convention `onUppercase`
* - `CancellationToken` is currently not supported
* - if a context is provided, you can use `AddFirstParameterToFunctions`
* utility to signal this in the receiving side type
*/
export interface IBaseChannelOptions {
/**
* Disables automatic marshalling of `URI`.
* If marshalling is disabled, `UriComponents`
* must be used instead.
*/
disableMarshalling?: boolean;
}
export interface IChannelReceiverOptions extends IBaseChannelOptions { }
export function createChannelReceiver(service: unknown, options?: IChannelReceiverOptions): IServerChannel {
const handler = service as { [key: string]: unknown };
const disableMarshalling = options && options.disableMarshalling;
// Buffer any event that should be supported by
// iterating over all property keys and finding them
const mapEventNameToEvent = new Map<string, Event<unknown>>();
for (const key in handler) {
if (propertyIsEvent(key)) {
mapEventNameToEvent.set(key, Event.buffer(handler[key] as Event<unknown>, true));
}
}
return new class implements IServerChannel {
listen<T>(_: unknown, event: string): Event<T> {
const eventImpl = mapEventNameToEvent.get(event);
if (eventImpl) {
return eventImpl as Event<T>;
}
throw new Error(`Event not found: ${event}`);
}
call(_: unknown, command: string, args?: any[]): Promise<any> {
const target = handler[command];
if (typeof target === 'function') {
// Revive unless marshalling disabled
if (!disableMarshalling && Array.isArray(args)) {
for (let i = 0; i < args.length; i++) {
args[i] = revive(args[i]);
}
}
return target.apply(handler, args);
}
throw new Error(`Method not found: ${command}`);
}
};
}
export interface IChannelSenderOptions extends IBaseChannelOptions {
/**
* If provided, will add the value of `context`
* to each method call to the target.
*/
context?: unknown;
}
export function createChannelSender<T>(channel: IChannel, options?: IChannelSenderOptions): T {
const disableMarshalling = options && options.disableMarshalling;
return new Proxy({}, {
get(_target: T, propKey: PropertyKey) {
if (typeof propKey === 'string') {
// Event
if (propertyIsEvent(propKey)) {
return channel.listen(propKey);
}
// Function
return async function (...args: any[]) {
// Add context if any
let methodArgs: any[];
if (options && !isUndefinedOrNull(options.context)) {
methodArgs = [options.context, ...args];
} else {
methodArgs = args;
}
const result = await channel.call(propKey, methodArgs);
// Revive unless marshalling disabled
if (!disableMarshalling) {
return revive(result);
}
return result;
};
}
throw new Error(`Property not found: ${String(propKey)}`);
}
}) as T;
}
function propertyIsEvent(name: string): boolean {
// Assume a property is an event if it has a form of "onSomething"
return name[0] === 'o' && name[1] === 'n' && isUpperAsciiLetter(name.charCodeAt(2));
}

View File

@@ -4,8 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { IChannel, IServerChannel, IMessagePassingProtocol, IPCServer, ClientConnectionEvent, IPCClient } from 'vs/base/parts/ipc/common/ipc';
import { createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/node/ipc';
import { IChannel, IServerChannel, IMessagePassingProtocol, IPCServer, ClientConnectionEvent, IPCClient, createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/common/ipc';
import { Emitter, Event } from 'vs/base/common/event';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { canceled } from 'vs/base/common/errors';
@@ -103,7 +102,7 @@ interface ITestService {
error(message: string): Promise<void>;
neverComplete(): Promise<void>;
neverCompleteCT(cancellationToken: CancellationToken): Promise<void>;
buffersLength(buffers: Buffer[]): Promise<number>;
buffersLength(buffers: VSBuffer[]): Promise<number>;
marshall(uri: URI): Promise<URI>;
context(): Promise<unknown>;
@@ -135,8 +134,8 @@ class TestService implements ITestService {
return new Promise((_, e) => cancellationToken.onCancellationRequested(() => e(canceled())));
}
buffersLength(buffers: Buffer[]): Promise<number> {
return Promise.resolve(buffers.reduce((r, b) => r + b.length, 0));
buffersLength(buffers: VSBuffer[]): Promise<number> {
return Promise.resolve(buffers.reduce((r, b) => r + b.buffer.length, 0));
}
ping(msg: string): void {
@@ -199,7 +198,7 @@ class TestChannelClient implements ITestService {
return this.channel.call('neverCompleteCT', undefined, cancellationToken);
}
buffersLength(buffers: Buffer[]): Promise<number> {
buffersLength(buffers: VSBuffer[]): Promise<number> {
return this.channel.call('buffersLength', buffers);
}
@@ -317,7 +316,7 @@ suite('Base IPC', function () {
});
test('buffers in arrays', async function () {
const r = await ipcService.buffersLength([Buffer.allocUnsafe(2), Buffer.allocUnsafe(3)]);
const r = await ipcService.buffersLength([VSBuffer.alloc(2), VSBuffer.alloc(3)]);
return assert.equal(r, 5);
});
});
@@ -383,7 +382,7 @@ suite('Base IPC', function () {
});
test('buffers in arrays', async function () {
const r = await ipcService.buffersLength([Buffer.allocUnsafe(2), Buffer.allocUnsafe(3)]);
const r = await ipcService.buffersLength([VSBuffer.alloc(2), VSBuffer.alloc(3)]);
return assert.equal(r, 5);
});
});

View File

@@ -307,7 +307,7 @@ export interface IQuickInputButton {
iconClass?: string;
tooltip?: string;
/**
* Wether to always show the button. By default buttons
* Whether to always show the button. By default buttons
* are only visible when hovering over them with the mouse
*/
alwaysVisible?: boolean;

View File

@@ -0,0 +1,249 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// #######################################################################
// ### ###
// ### electron.d.ts types we need in a common layer for reuse ###
// ### (copied from Electron 7.x) ###
// ### ###
// #######################################################################
export interface MessageBoxOptions {
/**
* Can be `"none"`, `"info"`, `"error"`, `"question"` or `"warning"`. On Windows,
* `"question"` displays the same icon as `"info"`, unless you set an icon using
* the `"icon"` option. On macOS, both `"warning"` and `"error"` display the same
* warning icon.
*/
type?: string;
/**
* Array of texts for buttons. On Windows, an empty array will result in one button
* labeled "OK".
*/
buttons?: string[];
/**
* Index of the button in the buttons array which will be selected by default when
* the message box opens.
*/
defaultId?: number;
/**
* Title of the message box, some platforms will not show it.
*/
title?: string;
/**
* Content of the message box.
*/
message: string;
/**
* Extra information of the message.
*/
detail?: string;
/**
* If provided, the message box will include a checkbox with the given label.
*/
checkboxLabel?: string;
/**
* Initial checked state of the checkbox. `false` by default.
*/
checkboxChecked?: boolean;
// icon?: NativeImage;
/**
* The index of the button to be used to cancel the dialog, via the `Esc` key. By
* default this is assigned to the first button with "cancel" or "no" as the label.
* If no such labeled buttons exist and this option is not set, `0` will be used as
* the return value.
*/
cancelId?: number;
/**
* On Windows Electron will try to figure out which one of the `buttons` are common
* buttons (like "Cancel" or "Yes"), and show the others as command links in the
* dialog. This can make the dialog appear in the style of modern Windows apps. If
* you don't like this behavior, you can set `noLink` to `true`.
*/
noLink?: boolean;
/**
* Normalize the keyboard access keys across platforms. Default is `false`.
* Enabling this assumes `&` is used in the button labels for the placement of the
* keyboard shortcut access key and labels will be converted so they work correctly
* on each platform, `&` characters are removed on macOS, converted to `_` on
* Linux, and left untouched on Windows. For example, a button label of `Vie&w`
* will be converted to `Vie_w` on Linux and `View` on macOS and can be selected
* via `Alt-W` on Windows and Linux.
*/
normalizeAccessKeys?: boolean;
}
export interface MessageBoxReturnValue {
/**
* The index of the clicked button.
*/
response: number;
/**
* The checked state of the checkbox if `checkboxLabel` was set. Otherwise `false`.
*/
checkboxChecked: boolean;
}
export interface OpenDevToolsOptions {
/**
* Opens the devtools with specified dock state, can be `right`, `bottom`,
* `undocked`, `detach`. Defaults to last used dock state. In `undocked` mode it's
* possible to dock back. In `detach` mode it's not.
*/
mode: ('right' | 'bottom' | 'undocked' | 'detach');
/**
* Whether to bring the opened devtools window to the foreground. The default is
* `true`.
*/
activate?: boolean;
}
export interface SaveDialogOptions {
title?: string;
/**
* Absolute directory path, absolute file path, or file name to use by default.
*/
defaultPath?: string;
/**
* Custom label for the confirmation button, when left empty the default label will
* be used.
*/
buttonLabel?: string;
filters?: FileFilter[];
/**
* Message to display above text fields.
*
* @platform darwin
*/
message?: string;
/**
* Custom label for the text displayed in front of the filename text field.
*
* @platform darwin
*/
nameFieldLabel?: string;
/**
* Show the tags input box, defaults to `true`.
*
* @platform darwin
*/
showsTagField?: boolean;
/**
* Create a security scoped bookmark when packaged for the Mac App Store. If this
* option is enabled and the file doesn't already exist a blank file will be
* created at the chosen path.
*
* @platform darwin,mas
*/
securityScopedBookmarks?: boolean;
}
export interface OpenDialogOptions {
title?: string;
defaultPath?: string;
/**
* Custom label for the confirmation button, when left empty the default label will
* be used.
*/
buttonLabel?: string;
filters?: FileFilter[];
/**
* Contains which features the dialog should use. The following values are
* supported:
*/
properties?: Array<'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles' | 'createDirectory' | 'promptToCreate' | 'noResolveAliases' | 'treatPackageAsDirectory'>;
/**
* Message to display above input boxes.
*
* @platform darwin
*/
message?: string;
/**
* Create security scoped bookmarks when packaged for the Mac App Store.
*
* @platform darwin,mas
*/
securityScopedBookmarks?: boolean;
}
export interface OpenDialogReturnValue {
/**
* whether or not the dialog was canceled.
*/
canceled: boolean;
/**
* An array of file paths chosen by the user. If the dialog is cancelled this will
* be an empty array.
*/
filePaths: string[];
/**
* An array matching the `filePaths` array of base64 encoded strings which contains
* security scoped bookmark data. `securityScopedBookmarks` must be enabled for
* this to be populated. (For return values, see table here.)
*
* @platform darwin,mas
*/
bookmarks?: string[];
}
export interface SaveDialogReturnValue {
/**
* whether or not the dialog was canceled.
*/
canceled: boolean;
/**
* If the dialog is canceled, this will be `undefined`.
*/
filePath?: string;
/**
* Base64 encoded string which contains the security scoped bookmark data for the
* saved file. `securityScopedBookmarks` must be enabled for this to be present.
* (For return values, see table here.)
*
* @platform darwin,mas
*/
bookmark?: string;
}
export interface CrashReporterStartOptions {
companyName: string;
/**
* URL that crash reports will be sent to as POST.
*/
submitURL: string;
/**
* Defaults to `app.name`.
*/
productName?: string;
/**
* Whether crash reports should be sent to the server. Default is `true`.
*/
uploadToServer?: boolean;
/**
* Default is `false`.
*/
ignoreSystemCrashHandler?: boolean;
/**
* An object you can define that will be sent along with the report. Only string
* properties are sent correctly. Nested objects are not supported. When using
* Windows, the property names and values must be fewer than 64 characters.
*/
extra?: Record<string, string>;
/**
* Directory to store the crash reports temporarily (only used when the crash
* reporter is started via `process.crashReporter.start`).
*/
crashesDirectory?: string;
}
export interface FileFilter {
// Docs: http://electronjs.org/docs/api/structures/file-filter
extensions: string[];
name: string;
}

View File

@@ -0,0 +1,115 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// @ts-check
(function () {
'use strict';
const { ipcRenderer, webFrame, crashReporter } = require('electron');
// @ts-ignore
window.vscode = {
/**
* A minimal set of methods exposed from ipcRenderer
* to support communication to electron-main
*
* @type {typeof import('../electron-sandbox/globals').ipcRenderer}
*/
ipcRenderer: {
/**
* @param {string} channel
* @param {any[]} args
*/
send(channel, ...args) {
validateIPC(channel);
ipcRenderer.send(channel, ...args);
},
/**
* @param {string} channel
* @param {(event: import('electron').IpcRendererEvent, ...args: any[]) => void} listener
*/
on(channel, listener) {
validateIPC(channel);
ipcRenderer.on(channel, listener);
},
/**
* @param {string} channel
* @param {(event: import('electron').IpcRendererEvent, ...args: any[]) => void} listener
*/
once(channel, listener) {
validateIPC(channel);
ipcRenderer.once(channel, listener);
},
/**
* @param {string} channel
* @param {(event: import('electron').IpcRendererEvent, ...args: any[]) => void} listener
*/
removeListener(channel, listener) {
validateIPC(channel);
ipcRenderer.removeListener(channel, listener);
}
},
/**
* Support for methods of webFrame type.
*
* @type {typeof import('../electron-sandbox/globals').webFrame}
*/
webFrame: {
getZoomFactor() {
return webFrame.getZoomFactor();
},
getZoomLevel() {
return webFrame.getZoomLevel();
},
/**
* @param {number} level
*/
setZoomLevel(level) {
webFrame.setZoomLevel(level);
}
},
/**
* Support for methods of crashReporter type.
*
* @type {typeof import('../electron-sandbox/globals').crashReporter}
*/
crashReporter: {
/**
* @param {Electron.CrashReporterStartOptions} options
*/
start(options) {
crashReporter.start(options);
}
}
};
//#region Utilities
/**
* @param {string} channel
*/
function validateIPC(channel) {
if (!channel || !channel.startsWith('vscode:')) {
throw new Error(`Unsupported event IPC channel '${channel}'`);
}
}
//#endregion
}());

View File

@@ -0,0 +1,93 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CrashReporterStartOptions } from 'vs/base/parts/sandbox/common/electronTypes';
export const ipcRenderer = (window as any).vscode.ipcRenderer as {
/**
* Listens to `channel`, when a new message arrives `listener` would be called with
* `listener(event, args...)`.
*/
on(channel: string, listener: (event: unknown, ...args: any[]) => void): void;
/**
* Adds a one time `listener` function for the event. This `listener` is invoked
* only the next time a message is sent to `channel`, after which it is removed.
*/
once(channel: string, listener: (event: unknown, ...args: any[]) => void): void;
/**
* Removes the specified `listener` from the listener array for the specified
* `channel`.
*/
removeListener(channel: string, listener: (event: unknown, ...args: any[]) => void): void;
/**
* Send an asynchronous message to the main process via `channel`, along with
* arguments. Arguments will be serialized with the Structured Clone Algorithm,
* just like `postMessage`, so prototype chains will not be included. Sending
* Functions, Promises, Symbols, WeakMaps, or WeakSets will throw an exception.
*
* > **NOTE**: Sending non-standard JavaScript types such as DOM objects or special
* Electron objects is deprecated, and will begin throwing an exception starting
* with Electron 9.
*
* The main process handles it by listening for `channel` with the `ipcMain`
* module.
*/
send(channel: string, ...args: any[]): void;
};
export const webFrame = (window as any).vscode.webFrame as {
/**
* The current zoom factor.
*/
getZoomFactor(): number;
/**
* The current zoom level.
*/
getZoomLevel(): number;
/**
* Changes the zoom level to the specified level. The original size is 0 and each
* increment above or below represents zooming 20% larger or smaller to default
* limits of 300% and 50% of original size, respectively.
*/
setZoomLevel(level: number): void;
};
export const crashReporter = (window as any).vscode.crashReporter as {
/**
* You are required to call this method before using any other `crashReporter` APIs
* and in each process (main/renderer) from which you want to collect crash
* reports. You can pass different options to `crashReporter.start` when calling
* from different processes.
*
* **Note** Child processes created via the `child_process` module will not have
* access to the Electron modules. Therefore, to collect crash reports from them,
* use `process.crashReporter.start` instead. Pass the same options as above along
* with an additional one called `crashesDirectory` that should point to a
* directory to store the crash reports temporarily. You can test this out by
* calling `process.crash()` to crash the child process.
*
* **Note:** If you need send additional/updated `extra` parameters after your
* first call `start` you can call `addExtraParameter` on macOS or call `start`
* again with the new/updated `extra` parameters on Linux and Windows.
*
* **Note:** On macOS and windows, Electron uses a new `crashpad` client for crash
* collection and reporting. If you want to enable crash reporting, initializing
* `crashpad` from the main process using `crashReporter.start` is required
* regardless of which process you want to collect crashes from. Once initialized
* this way, the crashpad handler collects crashes from all processes. You still
* have to call `crashReporter.start` from the renderer or child process, otherwise
* crashes from them will get reported without `companyName`, `productName` or any
* of the `extra` information.
*/
start(options: CrashReporterStartOptions): void;
};

View File

@@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { ipcRenderer, crashReporter, webFrame } from 'vs/base/parts/sandbox/electron-sandbox/globals';
suite('Sandbox', () => {
test('globals', () => {
assert.ok(ipcRenderer);
assert.ok(crashReporter);
assert.ok(webFrame);
});
});

View File

@@ -3,48 +3,294 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { compareFileNames, compareFileExtensions } from 'vs/base/common/comparers';
import { compareFileNames, compareFileExtensions, compareFileNamesNumeric, compareFileExtensionsNumeric } 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', () => {
//
// Comparisons with the same results as compareFileNamesNumeric
//
// name-only comparisons
assert(compareFileNames(null, null) === 0, 'null should be equal');
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('.abc', '.abc') === 0, 'equal full names should be equal');
assert(compareFileNames('.env', '.env.example') < 0, 'filenames with extensions should come after those without');
assert(compareFileNames('.env.example', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly');
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');
// name plus extension comparisons
assert(compareFileNames('bbb.aaa', 'aaa.bbb') > 0, 'files with extensions are compared first by filename');
// dotfile comparisons
assert(compareFileNames('.abc', '.abc') === 0, 'equal dotfile names should be equal');
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');
// dotfile vs non-dotfile comparisons
assert(compareFileNames(null, '.abc') < 0, 'null should come before dotfiles');
assert(compareFileNames('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions');
assert(compareFileNames('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions');
assert(compareFileNames('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files');
assert(compareFileNames('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files');
// numeric comparisons
assert(compareFileNames('1', '1') === 0, 'numerically equal full names should be equal');
assert(compareFileNames('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal');
assert(compareFileNames('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order');
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');
//
// Comparisons with different results than compareFileNamesNumeric
//
// 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.notDeepEqual(['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.notDeepEqual(['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');
// name plus extension comparisons
assert(compareFileNames('aggregate.go', 'aggregate_repo.go') > 0, 'compares the whole name all at once by locale');
// numeric comparisons
assert(compareFileNames('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort in unicode order');
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');
});
test('compareFileExtensions', () => {
//
// Comparisons with the same results as compareFileExtensionsNumeric
//
// name-only comparisons
assert(compareFileExtensions(null, null) === 0, 'null should be equal');
assert(compareFileExtensions(null, '.abc') < 0, 'null should come before real files');
assert(compareFileExtensions(null, 'abc') < 0, 'null should come before real files without extension');
assert(compareFileExtensions('', '') === 0, 'empty should be equal');
assert(compareFileExtensions('abc', 'abc') === 0, 'equal names should be equal');
assert(compareFileExtensions('.abc', '.abc') === 0, 'equal full names should be equal');
assert(compareFileExtensions('z', 'A') > 0, 'z comes after A');
assert(compareFileExtensions('Z', 'a') > 0, 'Z comes after a');
// name plus extension comparisons
assert(compareFileExtensions('file.ext', 'file.ext') === 0, 'equal full names should be equal');
assert(compareFileExtensions('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared');
assert(compareFileExtensions('.ext', 'a.ext') < 0, 'if equal extensions, filenames should be compared, empty filename should come before others');
assert(compareFileExtensions('file.aaa', 'file.bbb') < 0, 'files should be compared by extensions');
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');
assert(compareFileExtensions('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case');
// dotfile vs non-dotfile comparisons
assert(compareFileExtensions(null, '.abc') < 0, 'null should come before dotfiles');
assert(compareFileExtensions('.env', 'aaa.env') < 0, 'if equal extensions, filenames should be compared, empty filename should come before others');
assert(compareFileExtensions('.MD', 'a.md') < 0, 'if extensions differ in case, files sort by extension in unicode order');
// numeric comparisons
assert(compareFileExtensions('1', '1') === 0, 'numerically equal full names should be equal');
assert(compareFileExtensions('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal');
assert(compareFileExtensions('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order');
assert(compareFileExtensions('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long');
assert(compareFileExtensions('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically');
assert(compareFileExtensions('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number');
assert(compareFileExtensions('abc2.txt2', 'abc1.txt10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order');
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('file2.ext2', 'file1.ext10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order');
assert(compareFileExtensions('file.ext01', 'file.ext1') < 0, 'extensions with equal numbers should be in alphabetical order');
assert(compareFileExtensions('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically');
// Same extension comparison that has the same result as compareFileExtensionsNumeric, but a different result than compareFileNames
// This is an edge case caused by compareFileNames comparing the whole name all at once instead of the name and then the extension.
assert(compareFileExtensions('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, names sort in dictionary order');
//
// Comparisons with different results from compareFileExtensionsNumeric
//
// name-only comparisions
assert(compareFileExtensions('a', 'A') !== compareLocale('a', 'A'), 'the same letter of different case does not sort by locale');
assert(compareFileExtensions('â', 'Â') !== compareLocale('â', 'Â'), 'the same accented letter of different case does not sort by locale');
assert.notDeepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensions), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases do not sort in locale order');
assert.notDeepEqual(['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');
// dotfile comparisons
assert(compareFileExtensions('.env', '.aaa.env') < 0, 'a dotfile with an extension is treated as a name plus an extension - equal extensions');
assert(compareFileExtensions('.env', '.env.aaa') > 0, 'a dotfile with an extension is treated as a name plus an extension - unequal extensions');
// dotfile vs non-dotfile comparisons
assert(compareFileExtensions('.env', 'aaa') > 0, 'filenames without extensions come before dotfiles');
assert(compareFileExtensions('.md', 'A.MD') > 0, 'a file with an uppercase extension sorts before a dotfile of the same lowercase extension');
// numeric comparisons
assert(compareFileExtensions('abc.txt01', 'abc.txt1') < 0, 'extensions with equal numbers sort in unicode order');
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');
});
test('compareFileNamesNumeric', () => {
//
// Comparisons with the same results as compareFileNames
//
// name-only comparisons
assert(compareFileNamesNumeric(null, null) === 0, 'null should be equal');
assert(compareFileNamesNumeric(null, 'abc') < 0, 'null should be come before real values');
assert(compareFileNamesNumeric('', '') === 0, 'empty should be equal');
assert(compareFileNamesNumeric('abc', 'abc') === 0, 'equal names should be equal');
assert(compareFileNamesNumeric('z', 'A') > 0, 'z comes is after A regardless of case');
assert(compareFileNamesNumeric('Z', 'a') > 0, 'Z comes after a regardless of case');
// name plus extension comparisons
assert(compareFileNamesNumeric('file.ext', 'file.ext') === 0, 'equal full names should be equal');
assert(compareFileNamesNumeric('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared');
assert(compareFileNamesNumeric('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions');
assert(compareFileNamesNumeric('bbb.aaa', 'aaa.bbb') > 0, 'files should be compared by names even if extensions compare differently');
// dotfile comparisons
assert(compareFileNamesNumeric('.abc', '.abc') === 0, 'equal dotfile names should be equal');
assert(compareFileNamesNumeric('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly');
assert(compareFileNamesNumeric('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots');
assert(compareFileNamesNumeric('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first');
assert(compareFileNamesNumeric('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot');
// dotfile vs non-dotfile comparisons
assert(compareFileNamesNumeric(null, '.abc') < 0, 'null should come before dotfiles');
assert(compareFileNamesNumeric('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions');
assert(compareFileNamesNumeric('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions');
assert(compareFileNamesNumeric('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files');
assert(compareFileNamesNumeric('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files');
// numeric comparisons
assert(compareFileNamesNumeric('1', '1') === 0, 'numerically equal full names should be equal');
assert(compareFileNamesNumeric('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal');
assert(compareFileNamesNumeric('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order');
assert(compareFileNamesNumeric('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long');
assert(compareFileNamesNumeric('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically');
assert(compareFileNamesNumeric('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number');
//
// Comparisons with different results than compareFileNames
//
// name-only comparisons
assert(compareFileNamesNumeric('a', 'A') === compareLocale('a', 'A'), 'the same letter sorts by locale');
assert(compareFileNamesNumeric('â', 'Â') === compareLocale('â', 'Â'), 'the same accented letter sorts by locale');
assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesNumeric), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order');
assert.deepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNamesNumeric), ['email', 'Email', 'émail', 'Émail'].sort(compareLocale), 'the same base characters with different case or accents sort in locale order');
// name plus extensions comparisons
assert(compareFileNamesNumeric('aggregate.go', 'aggregate_repo.go') < 0, 'compares the name first, then the extension');
// numeric comparisons
assert(compareFileNamesNumeric('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first');
assert(compareFileNamesNumeric('abc.txt1', 'abc.txt01') < 0, 'same name plus extensions with equal numbers sort shortest number first');
assert(compareFileNamesNumeric('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale');
});
test('compareFileExtensionsNumeric', () => {
//
// Comparisons with the same result as compareFileExtensions
//
// name-only comparisons
assert(compareFileExtensionsNumeric(null, null) === 0, 'null should be equal');
assert(compareFileExtensionsNumeric(null, 'abc') < 0, 'null should come before real files without extensions');
assert(compareFileExtensionsNumeric('', '') === 0, 'empty should be equal');
assert(compareFileExtensionsNumeric('abc', 'abc') === 0, 'equal names should be equal');
assert(compareFileExtensionsNumeric('z', 'A') > 0, 'z comes after A');
assert(compareFileExtensionsNumeric('Z', 'a') > 0, 'Z comes after a');
// name plus extension comparisons
assert(compareFileExtensionsNumeric('file.ext', 'file.ext') === 0, 'equal full filenames should be equal');
assert(compareFileExtensionsNumeric('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared');
assert(compareFileExtensionsNumeric('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions');
assert(compareFileExtensionsNumeric('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first');
assert(compareFileExtensionsNumeric('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names');
assert(compareFileExtensionsNumeric('agg.go', 'agg_repo.go') < 0, 'shorter names short before longer names even when the longer name contains an underscore');
assert(compareFileExtensionsNumeric('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name');
// dotfile comparisons
assert(compareFileExtensionsNumeric('.abc', '.abc') === 0, 'equal dotfiles should be equal');
assert(compareFileExtensionsNumeric('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case');
// dotfile vs non-dotfile comparisons
assert(compareFileExtensionsNumeric(null, '.abc') < 0, 'null should come before dotfiles');
assert(compareFileExtensionsNumeric('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions');
assert(compareFileExtensionsNumeric('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files');
// numeric comparisons
assert(compareFileExtensionsNumeric('1', '1') === 0, 'numerically equal full names should be equal');
assert(compareFileExtensionsNumeric('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal');
assert(compareFileExtensionsNumeric('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order');
assert(compareFileExtensionsNumeric('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order');
assert(compareFileExtensionsNumeric('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically');
assert(compareFileExtensionsNumeric('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number');
assert(compareFileExtensionsNumeric('abc2.txt2', 'abc1.txt10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order');
assert(compareFileExtensionsNumeric('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal');
assert(compareFileExtensionsNumeric('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order');
assert(compareFileExtensionsNumeric('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long');
assert(compareFileExtensionsNumeric('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared');
assert(compareFileExtensionsNumeric('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically');
// Same extension comparison that has the same result as compareFileExtensions, but a different result than compareFileNames
// This is an edge case caused by compareFileNames comparing the whole name all at once instead of the name and then the extension.
assert(compareFileExtensionsNumeric('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, names sort in dictionary order');
//
// Comparisons with different results than compareFileExtensions
//
// name-only comparisons
assert(compareFileExtensionsNumeric('a', 'A') === compareLocale('a', 'A'), 'the same letter of different case sorts by locale');
assert(compareFileExtensionsNumeric('â', 'Â') === compareLocale('â', 'Â'), 'the same accented letter of different case sorts by locale');
assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsNumeric), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order');
assert.deepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensionsNumeric), ['email', 'Email', 'émail', 'Émail'].sort((a, b) => a.localeCompare(b)), 'the same base characters with different case or accents sort in locale order');
// name plus extension comparisons
assert(compareFileExtensionsNumeric('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale');
assert(compareFileExtensionsNumeric('a.md', 'A.md') === compareLocale('a', 'A'), 'case differences in names sort by locale');
// dotfile comparisons
assert(compareFileExtensionsNumeric('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots');
assert(compareFileExtensionsNumeric('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first');
// dotfile vs non-dotfile comparisons
assert(compareFileExtensionsNumeric('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions');
assert(compareFileExtensionsNumeric('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files');
// numeric comparisons
assert(compareFileExtensionsNumeric('abc.txt01', 'abc.txt1') > 0, 'extensions with equal numbers should be in shortest-first order');
assert(compareFileExtensionsNumeric('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale');
assert(compareFileExtensionsNumeric('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first');
assert(compareFileExtensionsNumeric('txt.abc01', 'txt.abc1') > 0, 'extensions with equivalent numbers sort shortest extension first');
});
});

View File

@@ -130,4 +130,41 @@ suite('Paths', () => {
assert.equal(extpath.indexOfPath('/some/long/path', '/some/long', false), 0);
assert.equal(extpath.indexOfPath('/some/long/path', '/PATH', true), 10);
});
test('parseLineAndColumnAware', () => {
let res = extpath.parseLineAndColumnAware('/foo/bar');
assert.equal(res.path, '/foo/bar');
assert.equal(res.line, undefined);
assert.equal(res.column, undefined);
res = extpath.parseLineAndColumnAware('/foo/bar:33');
assert.equal(res.path, '/foo/bar');
assert.equal(res.line, 33);
assert.equal(res.column, 1);
res = extpath.parseLineAndColumnAware('/foo/bar:33:34');
assert.equal(res.path, '/foo/bar');
assert.equal(res.line, 33);
assert.equal(res.column, 34);
res = extpath.parseLineAndColumnAware('C:\\foo\\bar');
assert.equal(res.path, 'C:\\foo\\bar');
assert.equal(res.line, undefined);
assert.equal(res.column, undefined);
res = extpath.parseLineAndColumnAware('C:\\foo\\bar:33');
assert.equal(res.path, 'C:\\foo\\bar');
assert.equal(res.line, 33);
assert.equal(res.column, 1);
res = extpath.parseLineAndColumnAware('C:\\foo\\bar:33:34');
assert.equal(res.path, 'C:\\foo\\bar');
assert.equal(res.line, 33);
assert.equal(res.column, 34);
res = extpath.parseLineAndColumnAware('/foo/bar:abb');
assert.equal(res.path, '/foo/bar:abb');
assert.equal(res.line, undefined);
assert.equal(res.column, undefined);
});
});

View File

@@ -13,8 +13,8 @@ suite('Map', () => {
let map = new LinkedMap<string, string>();
map.set('ak', 'av');
map.set('bk', 'bv');
assert.deepStrictEqual(map.keys(), ['ak', 'bk']);
assert.deepStrictEqual(map.values(), ['av', 'bv']);
assert.deepStrictEqual([...map.keys()], ['ak', 'bk']);
assert.deepStrictEqual([...map.values()], ['av', 'bv']);
assert.equal(map.first, 'av');
assert.equal(map.last, 'bv');
});
@@ -23,16 +23,16 @@ suite('Map', () => {
let map = new LinkedMap<string, string>();
map.set('ak', 'av');
map.set('ak', 'av', Touch.AsOld);
assert.deepStrictEqual(map.keys(), ['ak']);
assert.deepStrictEqual(map.values(), ['av']);
assert.deepStrictEqual([...map.keys()], ['ak']);
assert.deepStrictEqual([...map.values()], ['av']);
});
test('LinkedMap - Touch New one', () => {
let map = new LinkedMap<string, string>();
map.set('ak', 'av');
map.set('ak', 'av', Touch.AsNew);
assert.deepStrictEqual(map.keys(), ['ak']);
assert.deepStrictEqual(map.values(), ['av']);
assert.deepStrictEqual([...map.keys()], ['ak']);
assert.deepStrictEqual([...map.values()], ['av']);
});
test('LinkedMap - Touch Old two', () => {
@@ -40,8 +40,8 @@ suite('Map', () => {
map.set('ak', 'av');
map.set('bk', 'bv');
map.set('bk', 'bv', Touch.AsOld);
assert.deepStrictEqual(map.keys(), ['bk', 'ak']);
assert.deepStrictEqual(map.values(), ['bv', 'av']);
assert.deepStrictEqual([...map.keys()], ['bk', 'ak']);
assert.deepStrictEqual([...map.values()], ['bv', 'av']);
});
test('LinkedMap - Touch New two', () => {
@@ -49,8 +49,8 @@ suite('Map', () => {
map.set('ak', 'av');
map.set('bk', 'bv');
map.set('ak', 'av', Touch.AsNew);
assert.deepStrictEqual(map.keys(), ['bk', 'ak']);
assert.deepStrictEqual(map.values(), ['bv', 'av']);
assert.deepStrictEqual([...map.keys()], ['bk', 'ak']);
assert.deepStrictEqual([...map.values()], ['bv', 'av']);
});
test('LinkedMap - Touch Old from middle', () => {
@@ -59,8 +59,8 @@ suite('Map', () => {
map.set('bk', 'bv');
map.set('ck', 'cv');
map.set('bk', 'bv', Touch.AsOld);
assert.deepStrictEqual(map.keys(), ['bk', 'ak', 'ck']);
assert.deepStrictEqual(map.values(), ['bv', 'av', 'cv']);
assert.deepStrictEqual([...map.keys()], ['bk', 'ak', 'ck']);
assert.deepStrictEqual([...map.values()], ['bv', 'av', 'cv']);
});
test('LinkedMap - Touch New from middle', () => {
@@ -69,8 +69,8 @@ suite('Map', () => {
map.set('bk', 'bv');
map.set('ck', 'cv');
map.set('bk', 'bv', Touch.AsNew);
assert.deepStrictEqual(map.keys(), ['ak', 'ck', 'bk']);
assert.deepStrictEqual(map.values(), ['av', 'cv', 'bv']);
assert.deepStrictEqual([...map.keys()], ['ak', 'ck', 'bk']);
assert.deepStrictEqual([...map.values()], ['av', 'cv', 'bv']);
});
test('LinkedMap - basics', function () {
@@ -129,6 +129,61 @@ suite('Map', () => {
assert.ok(!map.has('1'));
});
test('LinkedMap - Iterators', () => {
const map = new LinkedMap<number, any>();
map.set(1, 1);
map.set(2, 2);
map.set(3, 3);
for (const elem of map.keys()) {
assert.ok(elem);
}
for (const elem of map.values()) {
assert.ok(elem);
}
for (const elem of map.entries()) {
assert.ok(elem);
}
{
const keys = map.keys();
const values = map.values();
const entries = map.entries();
map.get(1);
keys.next();
values.next();
entries.next();
}
{
const keys = map.keys();
const values = map.values();
const entries = map.entries();
map.get(1, Touch.AsNew);
let exceptions: number = 0;
try {
keys.next();
} catch (err) {
exceptions++;
}
try {
values.next();
} catch (err) {
exceptions++;
}
try {
entries.next();
} catch (err) {
exceptions++;
}
assert.strictEqual(exceptions, 3);
}
});
test('LinkedMap - LRU Cache simple', () => {
const cache = new LRUCache<number, number>(5);
@@ -136,10 +191,10 @@ suite('Map', () => {
assert.strictEqual(cache.size, 5);
cache.set(6, 6);
assert.strictEqual(cache.size, 5);
assert.deepStrictEqual(cache.keys(), [2, 3, 4, 5, 6]);
assert.deepStrictEqual([...cache.keys()], [2, 3, 4, 5, 6]);
cache.set(7, 7);
assert.strictEqual(cache.size, 5);
assert.deepStrictEqual(cache.keys(), [3, 4, 5, 6, 7]);
assert.deepStrictEqual([...cache.keys()], [3, 4, 5, 6, 7]);
let values: number[] = [];
[3, 4, 5, 6, 7].forEach(key => values.push(cache.get(key)!));
assert.deepStrictEqual(values, [3, 4, 5, 6, 7]);
@@ -150,11 +205,11 @@ suite('Map', () => {
[1, 2, 3, 4, 5].forEach(value => cache.set(value, value));
assert.strictEqual(cache.size, 5);
assert.deepStrictEqual(cache.keys(), [1, 2, 3, 4, 5]);
assert.deepStrictEqual([...cache.keys()], [1, 2, 3, 4, 5]);
cache.get(3);
assert.deepStrictEqual(cache.keys(), [1, 2, 4, 5, 3]);
assert.deepStrictEqual([...cache.keys()], [1, 2, 4, 5, 3]);
cache.peek(4);
assert.deepStrictEqual(cache.keys(), [1, 2, 4, 5, 3]);
assert.deepStrictEqual([...cache.keys()], [1, 2, 4, 5, 3]);
let values: number[] = [];
[1, 2, 3, 4, 5].forEach(key => values.push(cache.get(key)!));
assert.deepStrictEqual(values, [1, 2, 3, 4, 5]);
@@ -169,7 +224,7 @@ suite('Map', () => {
assert.strictEqual(cache.size, 10);
cache.limit = 5;
assert.strictEqual(cache.size, 5);
assert.deepStrictEqual(cache.keys(), [6, 7, 8, 9, 10]);
assert.deepStrictEqual([...cache.keys()], [6, 7, 8, 9, 10]);
cache.limit = 20;
assert.strictEqual(cache.size, 5);
for (let i = 11; i <= 20; i++) {
@@ -181,7 +236,7 @@ suite('Map', () => {
values.push(cache.get(i)!);
assert.strictEqual(cache.get(i), i);
}
assert.deepStrictEqual(cache.values(), values);
assert.deepStrictEqual([...cache.values()], values);
});
test('LinkedMap - LRU Cache limit with ratio', () => {
@@ -193,11 +248,11 @@ suite('Map', () => {
assert.strictEqual(cache.size, 10);
cache.set(11, 11);
assert.strictEqual(cache.size, 5);
assert.deepStrictEqual(cache.keys(), [7, 8, 9, 10, 11]);
assert.deepStrictEqual([...cache.keys()], [7, 8, 9, 10, 11]);
let values: number[] = [];
cache.keys().forEach(key => values.push(cache.get(key)!));
[...cache.keys()].forEach(key => values.push(cache.get(key)!));
assert.deepStrictEqual(values, [7, 8, 9, 10, 11]);
assert.deepStrictEqual(cache.values(), values);
assert.deepStrictEqual([...cache.values()], values);
});
test('LinkedMap - toJSON / fromJSON', () => {
@@ -222,7 +277,6 @@ suite('Map', () => {
assert.equal(key, 'ck');
assert.equal(value, 'cv');
}
i++;
});
});
@@ -237,7 +291,7 @@ suite('Map', () => {
map.delete('1');
assert.equal(map.get('1'), undefined);
assert.equal(map.size, 0);
assert.equal(map.keys().length, 0);
assert.equal([...map.keys()].length, 0);
});
test('LinkedMap - delete Head', function () {
@@ -251,8 +305,8 @@ suite('Map', () => {
map.delete('1');
assert.equal(map.get('2'), 2);
assert.equal(map.size, 1);
assert.equal(map.keys().length, 1);
assert.equal(map.keys()[0], 2);
assert.equal([...map.keys()].length, 1);
assert.equal([...map.keys()][0], 2);
});
test('LinkedMap - delete Tail', function () {
@@ -266,8 +320,8 @@ suite('Map', () => {
map.delete('2');
assert.equal(map.get('1'), 1);
assert.equal(map.size, 1);
assert.equal(map.keys().length, 1);
assert.equal(map.keys()[0], 1);
assert.equal([...map.keys()].length, 1);
assert.equal([...map.keys()][0], 1);
});
@@ -317,7 +371,8 @@ suite('Map', () => {
iter.reset(URI.parse('file:///usr/bin/file.txt'));
assert.equal(iter.value(), 'file');
assert.equal(iter.cmp('FILE'), 0);
// assert.equal(iter.cmp('FILE'), 0);
assert.equal(iter.cmp('file'), 0);
assert.equal(iter.hasNext(), true);
iter.next();
@@ -337,7 +392,8 @@ suite('Map', () => {
// scheme
assert.equal(iter.value(), 'file');
assert.equal(iter.cmp('FILE'), 0);
// assert.equal(iter.cmp('FILE'), 0);
assert.equal(iter.cmp('file'), 0);
assert.equal(iter.hasNext(), true);
iter.next();

View File

@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface Ctor<T> {
new(): T;
}
export function mock<T>(): Ctor<T> {
return function () { } as any;
}

View File

@@ -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 { dirname, basename, distinctParents, joinPath, isEqual, isEqualOrParent, hasToIgnoreCase, normalizePath, isAbsolutePath, relativePath, removeTrailingPathSeparator, hasTrailingPathSeparator, resolvePath, addTrailingPathSeparator, getComparisonKey } from 'vs/base/common/resources';
import { dirname, basename, distinctParents, joinPath, normalizePath, isAbsolutePath, relativePath, removeTrailingPathSeparator, hasTrailingPathSeparator, resolvePath, addTrailingPathSeparator, extUri, extUriIgnorePathCase } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { isWindows } from 'vs/base/common/platform';
import { toSlashes } from 'vs/base/common/extpath';
@@ -235,16 +235,19 @@ suite('Resources', () => {
}
});
function assertEqualURI(actual: URI, expected: URI, message?: string) {
if (!isEqual(expected, actual, hasToIgnoreCase(expected), false)) {
function assertEqualURI(actual: URI, expected: URI, message?: string, ignoreCase?: boolean) {
let util = ignoreCase ? extUriIgnorePathCase : extUri;
if (!util.isEqual(expected, actual)) {
assert.equal(actual.toString(), expected.toString(), message);
}
}
function assertRelativePath(u1: URI, u2: URI, expectedPath: string | undefined, ignoreJoin?: boolean, ignoreCase?: boolean) {
assert.equal(relativePath(u1, u2, ignoreCase), expectedPath, `from ${u1.toString()} to ${u2.toString()}`);
let util = ignoreCase ? extUriIgnorePathCase : extUri;
assert.equal(util.relativePath(u1, u2), expectedPath, `from ${u1.toString()} to ${u2.toString()}`);
if (expectedPath !== undefined && !ignoreJoin) {
assertEqualURI(removeTrailingPathSeparator(joinPath(u1, expectedPath)), removeTrailingPathSeparator(u2), 'joinPath on relativePath should be equal');
assertEqualURI(removeTrailingPathSeparator(joinPath(u1, expectedPath)), removeTrailingPathSeparator(u2), 'joinPath on relativePath should be equal', ignoreCase);
}
}
@@ -254,14 +257,14 @@ suite('Resources', () => {
assertRelativePath(URI.parse('foo://a/foo'), URI.parse('foo://a/foo/bar/goo'), 'bar/goo');
assertRelativePath(URI.parse('foo://a/'), URI.parse('foo://a/foo/bar/goo'), 'foo/bar/goo');
assertRelativePath(URI.parse('foo://a/foo/xoo'), URI.parse('foo://a/foo/bar'), '../bar');
assertRelativePath(URI.parse('foo://a/foo/xoo/yoo'), URI.parse('foo://a'), '../../..');
assertRelativePath(URI.parse('foo://a/foo/xoo/yoo'), URI.parse('foo://a'), '../../..', true);
assertRelativePath(URI.parse('foo://a/foo'), URI.parse('foo://a/foo/'), '');
assertRelativePath(URI.parse('foo://a/foo/'), URI.parse('foo://a/foo'), '');
assertRelativePath(URI.parse('foo://a/foo/'), URI.parse('foo://a/foo/'), '');
assertRelativePath(URI.parse('foo://a/foo'), URI.parse('foo://a/foo'), '');
assertRelativePath(URI.parse('foo://a'), URI.parse('foo://a'), '');
assertRelativePath(URI.parse('foo://a'), URI.parse('foo://a'), '', true);
assertRelativePath(URI.parse('foo://a/'), URI.parse('foo://a/'), '');
assertRelativePath(URI.parse('foo://a/'), URI.parse('foo://a'), '');
assertRelativePath(URI.parse('foo://a/'), URI.parse('foo://a'), '', true);
assertRelativePath(URI.parse('foo://a/foo?q'), URI.parse('foo://a/foo/bar#h'), 'bar', true);
assertRelativePath(URI.parse('foo://'), URI.parse('foo://a/b'), undefined);
assertRelativePath(URI.parse('foo://a2/b'), URI.parse('foo://a/b'), undefined);
@@ -346,10 +349,17 @@ suite('Resources', () => {
});
function assertIsEqual(u1: URI, u2: URI, ignoreCase: boolean, expected: boolean) {
assert.equal(isEqual(u1, u2, ignoreCase), expected, `${u1.toString()}${expected ? '===' : '!=='}${u2.toString()}`);
assert.equal(getComparisonKey(u1, ignoreCase) === getComparisonKey(u2, ignoreCase), expected, `comparison keys ${u1.toString()}, ${u2.toString()}`);
assert.equal(isEqualOrParent(u1, u2, ignoreCase), expected, `isEqualOrParent ${u1.toString()}, ${u2.toString()}`);
function assertIsEqual(u1: URI, u2: URI, ignoreCase: boolean | undefined, expected: boolean) {
let util = ignoreCase ? extUriIgnorePathCase : extUri;
assert.equal(util.isEqual(u1, u2), expected, `${u1.toString()}${expected ? '===' : '!=='}${u2.toString()}`);
assert.equal(util.compare(u1, u2) === 0, expected);
assert.equal(util.getComparisonKey(u1) === util.getComparisonKey(u2), expected, `comparison keys ${u1.toString()}, ${u2.toString()}`);
assert.equal(util.isEqualOrParent(u1, u2), expected, `isEqualOrParent ${u1.toString()}, ${u2.toString()}`);
if (!ignoreCase) {
assert.equal(u1.toString() === u2.toString(), expected);
}
}
@@ -358,7 +368,7 @@ suite('Resources', () => {
let fileURI2 = isWindows ? URI.file('C:\\foo\\Bar') : URI.file('/foo/Bar');
assertIsEqual(fileURI, fileURI, true, true);
assertIsEqual(fileURI, fileURI, false, true);
assertIsEqual(fileURI, fileURI, hasToIgnoreCase(fileURI), true);
assertIsEqual(fileURI, fileURI, undefined, true);
assertIsEqual(fileURI, fileURI2, true, true);
assertIsEqual(fileURI, fileURI2, false, false);
@@ -366,13 +376,15 @@ suite('Resources', () => {
let fileURI4 = URI.parse('foo://server:453/foo/Bar');
assertIsEqual(fileURI3, fileURI3, true, true);
assertIsEqual(fileURI3, fileURI3, false, true);
assertIsEqual(fileURI3, fileURI3, hasToIgnoreCase(fileURI3), true);
assertIsEqual(fileURI3, fileURI3, undefined, true);
assertIsEqual(fileURI3, fileURI4, true, true);
assertIsEqual(fileURI3, fileURI4, false, false);
assertIsEqual(fileURI, fileURI3, true, false);
assertIsEqual(URI.parse('foo://server'), URI.parse('foo://server/'), true, true);
assertIsEqual(URI.parse('file://server'), URI.parse('file://server/'), true, true);
assertIsEqual(URI.parse('http://server'), URI.parse('http://server/'), true, true);
assertIsEqual(URI.parse('foo://server'), URI.parse('foo://server/'), true, false); // only selected scheme have / as the default path
assertIsEqual(URI.parse('foo://server/foo'), URI.parse('foo://server/foo/'), true, false);
assertIsEqual(URI.parse('foo://server/foo'), URI.parse('foo://server/foo?'), true, true);
@@ -383,38 +395,39 @@ suite('Resources', () => {
assertIsEqual(fileURI5, fileURI3, true, false);
assertIsEqual(fileURI6, fileURI6, true, true);
assertIsEqual(fileURI6, fileURI5, true, false);
assertIsEqual(fileURI6, fileURI3, true, true);
assertIsEqual(fileURI6, fileURI3, true, false);
});
test('isEqualOrParent', () => {
let fileURI = isWindows ? URI.file('c:\\foo\\bar') : URI.file('/foo/bar');
let fileURI2 = isWindows ? URI.file('c:\\foo') : URI.file('/foo');
let fileURI2b = isWindows ? URI.file('C:\\Foo\\') : URI.file('/Foo/');
assert.equal(isEqualOrParent(fileURI, fileURI, true), true, '1');
assert.equal(isEqualOrParent(fileURI, fileURI, false), true, '2');
assert.equal(isEqualOrParent(fileURI, fileURI2, true), true, '3');
assert.equal(isEqualOrParent(fileURI, fileURI2, false), true, '4');
assert.equal(isEqualOrParent(fileURI, fileURI2b, true), true, '5');
assert.equal(isEqualOrParent(fileURI, fileURI2b, false), false, '6');
assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI), true, '1');
assert.equal(extUri.isEqualOrParent(fileURI, fileURI), true, '2');
assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI2), true, '3');
assert.equal(extUri.isEqualOrParent(fileURI, fileURI2), true, '4');
assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI2b), true, '5');
assert.equal(extUri.isEqualOrParent(fileURI, fileURI2b), false, '6');
assert.equal(isEqualOrParent(fileURI2, fileURI, false), false, '7');
assert.equal(isEqualOrParent(fileURI2b, fileURI2, true), true, '8');
assert.equal(extUri.isEqualOrParent(fileURI2, fileURI), false, '7');
assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI2b, fileURI2), true, '8');
let fileURI3 = URI.parse('foo://server:453/foo/bar/goo');
let fileURI4 = URI.parse('foo://server:453/foo/');
let fileURI5 = URI.parse('foo://server:453/foo');
assert.equal(isEqualOrParent(fileURI3, fileURI3, true), true, '11');
assert.equal(isEqualOrParent(fileURI3, fileURI3, false), true, '12');
assert.equal(isEqualOrParent(fileURI3, fileURI4, true), true, '13');
assert.equal(isEqualOrParent(fileURI3, fileURI4, false), true, '14');
assert.equal(isEqualOrParent(fileURI3, fileURI, true), false, '15');
assert.equal(isEqualOrParent(fileURI5, fileURI5, true), true, '16');
assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI3, true), true, '11');
assert.equal(extUri.isEqualOrParent(fileURI3, fileURI3), true, '12');
assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI4, true), true, '13');
assert.equal(extUri.isEqualOrParent(fileURI3, fileURI4), true, '14');
assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI, true), false, '15');
assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI5, fileURI5, true), true, '16');
let fileURI6 = URI.parse('foo://server:453/foo?q=1');
let fileURI7 = URI.parse('foo://server:453/foo/bar?q=1');
assert.equal(isEqualOrParent(fileURI6, fileURI5, true), false, '17');
assert.equal(isEqualOrParent(fileURI6, fileURI6, true), true, '18');
assert.equal(isEqualOrParent(fileURI7, fileURI6, true), true, '19');
assert.equal(isEqualOrParent(fileURI7, fileURI5, true), false, '20');
assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI6, fileURI5), false, '17');
assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI6, fileURI6), true, '18');
assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI7, fileURI6), true, '19');
assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI7, fileURI5), false, '20');
});
});

View File

@@ -0,0 +1,218 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { SkipList } from 'vs/base/common/skipList';
import { StopWatch } from 'vs/base/common/stopwatch';
import { binarySearch } from 'vs/base/common/arrays';
suite('SkipList', function () {
function assertValues<V>(list: SkipList<any, V>, expected: V[]) {
assert.equal(list.size, expected.length);
assert.deepEqual([...list.values()], expected);
let valuesFromEntries = [...list.entries()].map(entry => entry[1]);
assert.deepEqual(valuesFromEntries, expected);
let valuesFromIter = [...list].map(entry => entry[1]);
assert.deepEqual(valuesFromIter, expected);
let i = 0;
list.forEach((value, _key, map) => {
assert.ok(map === list);
assert.deepEqual(value, expected[i++]);
});
}
function assertKeys<K>(list: SkipList<K, any>, expected: K[]) {
assert.equal(list.size, expected.length);
assert.deepEqual([...list.keys()], expected);
let keysFromEntries = [...list.entries()].map(entry => entry[0]);
assert.deepEqual(keysFromEntries, expected);
let keysFromIter = [...list].map(entry => entry[0]);
assert.deepEqual(keysFromIter, expected);
let i = 0;
list.forEach((_value, key, map) => {
assert.ok(map === list);
assert.deepEqual(key, expected[i++]);
});
}
test('set/get/delete', function () {
let list = new SkipList<number, number>((a, b) => a - b);
assert.equal(list.get(3), undefined);
list.set(3, 1);
assert.equal(list.get(3), 1);
assertValues(list, [1]);
list.set(3, 3);
assertValues(list, [3]);
list.set(1, 1);
list.set(4, 4);
assert.equal(list.get(3), 3);
assert.equal(list.get(1), 1);
assert.equal(list.get(4), 4);
assertValues(list, [1, 3, 4]);
assert.equal(list.delete(17), false);
assert.equal(list.delete(1), true);
assert.equal(list.get(1), undefined);
assert.equal(list.get(3), 3);
assert.equal(list.get(4), 4);
assertValues(list, [3, 4]);
});
test('Figure 3', function () {
let list = new SkipList<number, boolean>((a, b) => a - b);
list.set(3, true);
list.set(6, true);
list.set(7, true);
list.set(9, true);
list.set(12, true);
list.set(19, true);
list.set(21, true);
list.set(25, true);
assertKeys(list, [3, 6, 7, 9, 12, 19, 21, 25]);
list.set(17, true);
assert.deepEqual(list.size, 9);
assertKeys(list, [3, 6, 7, 9, 12, 17, 19, 21, 25]);
});
test('capacity max', function () {
let list = new SkipList<number, boolean>((a, b) => a - b, 10);
list.set(1, true);
list.set(2, true);
list.set(3, true);
list.set(4, true);
list.set(5, true);
list.set(6, true);
list.set(7, true);
list.set(8, true);
list.set(9, true);
list.set(10, true);
list.set(11, true);
list.set(12, true);
assertKeys(list, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
});
const cmp = (a: number, b: number): number => {
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
};
function insertArraySorted(array: number[], element: number) {
let idx = binarySearch(array, element, cmp);
if (idx >= 0) {
array[idx] = element;
} else {
idx = ~idx;
// array = array.slice(0, idx).concat(element, array.slice(idx));
array.splice(idx, 0, element);
}
return array;
}
function delArraySorted(array: number[], element: number) {
let idx = binarySearch(array, element, cmp);
if (idx >= 0) {
// array = array.slice(0, idx).concat(array.slice(idx));
array.splice(idx, 1);
}
return array;
}
test('perf', function () {
this.skip();
// data
const max = 2 ** 16;
const values = new Set<number>();
for (let i = 0; i < max; i++) {
let value = Math.floor(Math.random() * max);
values.add(value);
}
console.log(values.size);
// init
let list = new SkipList<number, boolean>(cmp, max);
let sw = new StopWatch(true);
values.forEach(value => list.set(value, true));
sw.stop();
console.log(`[LIST] ${list.size} elements after ${sw.elapsed()}ms`);
let array: number[] = [];
sw = new StopWatch(true);
values.forEach(value => array = insertArraySorted(array, value));
sw.stop();
console.log(`[ARRAY] ${array.length} elements after ${sw.elapsed()}ms`);
// get
sw = new StopWatch(true);
let someValues = [...values].slice(0, values.size / 4);
someValues.forEach(key => {
let value = list.get(key); // find
console.assert(value, '[LIST] must have ' + key);
list.get(-key); // miss
});
sw.stop();
console.log(`[LIST] retrieve ${sw.elapsed()}ms (${(sw.elapsed() / (someValues.length * 2)).toPrecision(4)}ms/op)`);
sw = new StopWatch(true);
someValues.forEach(key => {
let idx = binarySearch(array, key, cmp); // find
console.assert(idx >= 0, '[ARRAY] must have ' + key);
binarySearch(array, -key, cmp); // miss
});
sw.stop();
console.log(`[ARRAY] retrieve ${sw.elapsed()}ms (${(sw.elapsed() / (someValues.length * 2)).toPrecision(4)}ms/op)`);
// insert
sw = new StopWatch(true);
someValues.forEach(key => {
list.set(-key, false);
});
sw.stop();
console.log(`[LIST] insert ${sw.elapsed()}ms (${(sw.elapsed() / someValues.length).toPrecision(4)}ms/op)`);
sw = new StopWatch(true);
someValues.forEach(key => {
array = insertArraySorted(array, -key);
});
sw.stop();
console.log(`[ARRAY] insert ${sw.elapsed()}ms (${(sw.elapsed() / someValues.length).toPrecision(4)}ms/op)`);
// delete
sw = new StopWatch(true);
someValues.forEach(key => {
list.delete(key); // find
list.delete(-key); // miss
});
sw.stop();
console.log(`[LIST] delete ${sw.elapsed()}ms (${(sw.elapsed() / (someValues.length * 2)).toPrecision(4)}ms/op)`);
sw = new StopWatch(true);
someValues.forEach(key => {
array = delArraySorted(array, key); // find
array = delArraySorted(array, -key); // miss
});
sw.stop();
console.log(`[ARRAY] delete ${sw.elapsed()}ms (${(sw.elapsed() / (someValues.length * 2)).toPrecision(4)}ms/op)`);
});
});