Merge branch 'splitview' into scm-viewlet

This commit is contained in:
Joao Moreno
2017-09-18 11:32:04 +02:00
239 changed files with 5624 additions and 3555 deletions

View File

@@ -11,7 +11,7 @@ import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlighte
import { IMatch } from 'vs/base/common/filters';
import uri from 'vs/base/common/uri';
import paths = require('vs/base/common/paths');
import { IRootProvider, getPathLabel, IUserHomeProvider } from 'vs/base/common/labels';
import { IWorkspaceFolderProvider, getPathLabel, IUserHomeProvider } from 'vs/base/common/labels';
import { IDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
export interface IIconLabelCreationOptions {
@@ -152,13 +152,13 @@ export class IconLabel {
export class FileLabel extends IconLabel {
constructor(container: HTMLElement, file: uri, provider: IRootProvider, userHome?: IUserHomeProvider) {
constructor(container: HTMLElement, file: uri, provider: IWorkspaceFolderProvider, userHome?: IUserHomeProvider) {
super(container);
this.setFile(file, provider, userHome);
}
public setFile(file: uri, provider: IRootProvider, userHome: IUserHomeProvider): void {
public setFile(file: uri, provider: IWorkspaceFolderProvider, userHome: IUserHomeProvider): void {
const parent = paths.dirname(file.fsPath);
this.setValue(paths.basename(file.fsPath), parent && parent !== '.' ? getPathLabel(parent, provider, userHome) : '', { title: file.fsPath });

View File

@@ -398,7 +398,7 @@ class MouseController<T> implements IDisposable {
if (isSelectionRangeChangeEvent(e) && reference !== undefined) {
const min = Math.min(reference, focus);
const max = Math.max(reference, focus);
const rangeSelection = range(max + 1, min);
const rangeSelection = range(min, max + 1);
const selection = this.list.getSelection();
const contiguousRange = getContiguousRangeContaining(disjunction(selection, [reference]), reference);

View File

@@ -268,6 +268,10 @@ export class Sash extends EventEmitter {
this.isDisabled = true;
}
get enabled(): boolean {
return !this.isDisabled;
}
public dispose(): void {
if (this.$e) {
this.$e.destroy();

View File

@@ -0,0 +1,361 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./splitview';
import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle';
import Event, { Emitter, chain } from 'vs/base/common/event';
import { domEvent } from 'vs/base/browser/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { $, append, addClass, removeClass, toggleClass } from 'vs/base/browser/dom';
import { firstIndex } from 'vs/base/common/arrays';
import { Color, RGBA } from 'vs/base/common/color';
import { SplitView, IView } from './splitview2';
export interface IPanelOptions {
ariaHeaderLabel?: string;
minimumBodySize?: number;
maximumBodySize?: number;
expanded?: boolean;
}
export interface IPanelStyles {
dropBackground?: Color;
}
export abstract class Panel implements IView {
private static HEADER_SIZE = 22;
private _expanded: boolean;
private _headerVisible: boolean;
private _onDidChange = new Emitter<void>();
private _minimumBodySize: number;
private _maximumBodySize: number;
private ariaHeaderLabel: string;
readonly header: HTMLElement;
protected disposables: IDisposable[] = [];
get minimumBodySize(): number {
return this._minimumBodySize;
}
set minimumBodySize(size: number) {
this._minimumBodySize = size;
this._onDidChange.fire();
}
get maximumBodySize(): number {
return this._maximumBodySize;
}
set maximumBodySize(size: number) {
this._maximumBodySize = size;
this._onDidChange.fire();
}
get minimumSize(): number {
const headerSize = this.headerVisible ? Panel.HEADER_SIZE : 0;
const expanded = !this.headerVisible || this.expanded;
const minimumBodySize = expanded ? this._minimumBodySize : 0;
return headerSize + minimumBodySize;
}
get maximumSize(): number {
const headerSize = this.headerVisible ? Panel.HEADER_SIZE : 0;
const expanded = !this.headerVisible || this.expanded;
const maximumBodySize = expanded ? this._maximumBodySize : 0;
return headerSize + maximumBodySize;
}
readonly onDidChange: Event<void> = this._onDidChange.event;
constructor(options: IPanelOptions = {}) {
this._expanded = typeof options.expanded === 'undefined' ? true : !!options.expanded;
this.ariaHeaderLabel = options.ariaHeaderLabel || '';
this._minimumBodySize = typeof options.minimumBodySize === 'number' ? options.minimumBodySize : 44;
this._maximumBodySize = typeof options.maximumBodySize === 'number' ? options.maximumBodySize : Number.POSITIVE_INFINITY;
}
get expanded(): boolean {
return this._expanded;
}
set expanded(expanded: boolean) {
if (this._expanded === !!expanded) {
return;
}
this._expanded = !!expanded;
this.renderHeader();
this._onDidChange.fire();
}
get headerVisible(): boolean {
return this._headerVisible;
}
set headerVisible(visible: boolean) {
if (this._headerVisible === !!visible) {
return;
}
this._headerVisible = !!visible;
this.renderHeader();
this._onDidChange.fire();
}
render(container: HTMLElement): void {
const panel = append(container, $('.panel'));
const header = append(panel, $('.panel-header'));
header.setAttribute('tabindex', '0');
header.setAttribute('role', 'toolbar');
header.setAttribute('aria-label', this.ariaHeaderLabel);
this.renderHeader();
const onHeaderKeyDown = chain(domEvent(header, 'keydown'))
.map(e => new StandardKeyboardEvent(e));
onHeaderKeyDown.filter(e => e.keyCode === KeyCode.Enter || e.keyCode === KeyCode.Space)
.event(() => this.expanded = !this.expanded, null, this.disposables);
onHeaderKeyDown.filter(e => e.keyCode === KeyCode.LeftArrow)
.event(() => this.expanded = false, null, this.disposables);
onHeaderKeyDown.filter(e => e.keyCode === KeyCode.RightArrow)
.event(() => this.expanded = true, null, this.disposables);
domEvent(header, 'click')
(() => this.expanded = !this.expanded, null, this.disposables);
// TODO@Joao move this down to panelview
// onHeaderKeyDown.filter(e => e.keyCode === KeyCode.UpArrow)
// .event(focusPrevious, this, this.disposables);
// onHeaderKeyDown.filter(e => e.keyCode === KeyCode.DownArrow)
// .event(focusNext, this, this.disposables);
const body = append(panel, $('.panel-body'));
this.renderBody(body);
}
layout(size: number): void {
const headerSize = this.headerVisible ? Panel.HEADER_SIZE : 0;
this.layoutBody(size - headerSize);
}
focus(): void {
// TODO@joao what to do
}
private renderHeader(): void {
const expanded = !this.headerVisible || this.expanded;
toggleClass(this.header, 'hidden', !this.headerVisible);
toggleClass(this.header, 'expanded', expanded);
this.header.setAttribute('aria-expanded', String(expanded));
}
protected abstract renderBody(container: HTMLElement): void;
protected abstract layoutBody(size: number): void;
dispose(): void {
this.disposables = dispose(this.disposables);
}
}
interface IDndContext {
dropBackground: Color | undefined;
draggable: PanelDraggable | null;
}
class PanelDraggable implements IDisposable {
private static DefaultDragOverBackgroundColor = new Color(new RGBA(128, 128, 128, 0.5));
// see https://github.com/Microsoft/vscode/issues/14470
private dragOverCounter = 0;
private disposables: IDisposable[] = [];
private _onDidDrop = new Emitter<{ from: Panel, to: Panel }>();
readonly onDidDrop = this._onDidDrop.event;
constructor(private panel: Panel, private context: IDndContext) {
domEvent(panel.header, 'dragstart')(this.onDragStart, this, this.disposables);
domEvent(panel.header, 'dragenter')(this.onDragEnter, this, this.disposables);
domEvent(panel.header, 'dragleave')(this.onDragLeave, this, this.disposables);
domEvent(panel.header, 'dragend')(this.onDragEnd, this, this.disposables);
domEvent(panel.header, 'drop')(this.onDrop, this, this.disposables);
}
private onDragStart(e: DragEvent): void {
e.dataTransfer.effectAllowed = 'move';
const dragImage = append(document.body, $('.monaco-panel-drag-image', {}, this.panel.header.textContent));
e.dataTransfer.setDragImage(dragImage, -10, -10);
setTimeout(() => document.body.removeChild(dragImage), 0);
this.context.draggable = this;
}
private onDragEnter(e: DragEvent): void {
if (!this.context.draggable || this.context.draggable === this) {
return;
}
this.dragOverCounter++;
this.renderHeader();
}
private onDragLeave(e: DragEvent): void {
if (!this.context.draggable || this.context.draggable === this) {
return;
}
this.dragOverCounter--;
if (this.dragOverCounter === 0) {
this.renderHeader();
}
}
private onDragEnd(e: DragEvent): void {
if (!this.context.draggable) {
return;
}
this.dragOverCounter = 0;
this.renderHeader();
this.context.draggable = null;
}
private onDrop(e: DragEvent): void {
if (!this.context.draggable) {
return;
}
this.dragOverCounter = 0;
this.renderHeader();
if (this.context.draggable !== this) {
this._onDidDrop.fire({ from: this.context.draggable.panel, to: this.panel });
}
this.context.draggable = null;
}
private renderHeader(): void {
let backgroundColor: string = null;
if (this.dragOverCounter > 0) {
backgroundColor = (this.context.dropBackground || PanelDraggable.DefaultDragOverBackgroundColor).toString();
}
this.panel.header.style.backgroundColor = backgroundColor;
}
dispose(): void {
this.disposables = dispose(this.disposables);
}
}
export class IPanelViewOptions {
dnd?: boolean;
}
interface IPanelItem {
panel: Panel;
disposable: IDisposable;
}
export class PanelView implements IDisposable {
private dnd: boolean;
private dndContext: IDndContext = { dropBackground: undefined, draggable: null };
private el: HTMLElement;
private panelItems: IPanelItem[] = [];
private splitview: SplitView;
private animationTimer: number | null = null;
private _onDidDrop = new Emitter<{ from: Panel, to: Panel }>();
readonly onDidDrop: Event<{ from: Panel, to: Panel }> = this._onDidDrop.event;
constructor(private container: HTMLElement, options?: IPanelViewOptions) {
this.dnd = !!options.dnd;
this.el = append(container, $('.monaco-panel-view'));
this.splitview = new SplitView(container);
}
addPanel(panel: Panel, size: number, index = this.splitview.length): void {
const disposables: IDisposable[] = [];
panel.onDidChange(this.setupAnimation, this, disposables);
if (this.dnd) {
const draggable = new PanelDraggable(panel, this.dndContext);
disposables.push(draggable);
draggable.onDidDrop(this._onDidDrop.fire, this._onDidDrop, disposables);
}
const panelItem = { panel, disposable: combinedDisposable(disposables) };
this.panelItems.splice(index, 0, panelItem);
this.splitview.addView(panel, size, index);
}
removePanel(panel: Panel): void {
const index = firstIndex(this.panelItems, item => item.panel === panel);
if (index === -1) {
return;
}
this.splitview.removeView(index);
const panelItem = this.panelItems.splice(index, 1)[0];
panelItem.disposable.dispose();
}
movePanel(from: Panel, to: Panel): void {
const fromIndex = firstIndex(this.panelItems, item => item.panel === from);
const toIndex = firstIndex(this.panelItems, item => item.panel === to);
if (fromIndex === -1 || toIndex === -1) {
return;
}
this.splitview.moveView(fromIndex, toIndex);
}
layout(size: number): void {
this.splitview.layout(size);
}
style(styles: IPanelStyles): void {
this.dndContext.dropBackground = styles.dropBackground;
}
private setupAnimation(): void {
if (typeof this.animationTimer === 'number') {
window.clearTimeout(this.animationTimer);
}
addClass(this.el, 'animated');
this.animationTimer = window.setTimeout(() => {
this.animationTimer = null;
removeClass(this.el, 'animated');
}, 200);
}
dispose(): void {
this.panelItems.forEach(i => i.disposable.dispose());
this.splitview.dispose();
}
}

View File

@@ -0,0 +1,293 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./splitview';
import { IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle';
import Event, { fromEventEmitter, mapEvent } from 'vs/base/common/event';
import types = require('vs/base/common/types');
import dom = require('vs/base/browser/dom');
import { clamp } from 'vs/base/common/numbers';
import { range, firstIndex } from 'vs/base/common/arrays';
import { Sash, Orientation, ISashEvent as IBaseSashEvent } from 'vs/base/browser/ui/sash/sash';
export { Orientation } from 'vs/base/browser/ui/sash/sash';
export interface ISplitViewOptions {
orientation?: Orientation; // default Orientation.VERTICAL
}
export interface IView {
readonly minimumSize: number;
readonly maximumSize: number;
readonly onDidChange: Event<void>;
render(container: HTMLElement, orientation: Orientation): void;
layout(size: number, orientation: Orientation): void;
focus(): void;
}
interface ISashEvent {
sash: Sash;
start: number;
current: number;
}
interface IViewItem {
view: IView;
size: number;
explicitSize: number;
container: HTMLElement;
disposable: IDisposable;
}
interface ISashItem {
sash: Sash;
disposable: IDisposable;
}
interface ISashDragState {
index: number;
start: number;
sizes: number[];
}
function layoutViewItem(item: IViewItem, orientation: Orientation): void {
if (orientation === Orientation.VERTICAL) {
item.container.style.height = `${item.size}px`;
} else {
item.container.style.width = `${item.size}px`;
}
item.view.layout(item.size, orientation);
}
export class SplitView implements IDisposable {
private orientation: Orientation;
private el: HTMLElement;
private size = 0;
private viewItems: IViewItem[] = [];
private sashItems: ISashItem[] = [];
private sashDragState: ISashDragState;
get length(): number {
return this.viewItems.length;
}
constructor(private container: HTMLElement, options: ISplitViewOptions = {}) {
this.orientation = types.isUndefined(options.orientation) ? Orientation.VERTICAL : options.orientation;
this.el = document.createElement('div');
dom.addClass(this.el, 'monaco-split-view');
dom.addClass(this.el, this.orientation === Orientation.VERTICAL ? 'vertical' : 'horizontal');
container.appendChild(this.el);
}
addView(view: IView, size: number, index = this.viewItems.length): void {
// Add view
const container = dom.$('.split-view-view');
if (this.viewItems.length === 1) {
this.el.appendChild(container);
} else {
this.el.insertBefore(container, this.el.children.item(index));
}
const onChangeDisposable = mapEvent(view.onDidChange, () => item)(this.onViewChange, this);
const containerDisposable = toDisposable(() => this.el.removeChild(container));
const disposable = combinedDisposable([onChangeDisposable, containerDisposable]);
const explicitSize = size;
const item: IViewItem = { view, container, explicitSize, size, disposable };
this.viewItems.splice(index, 0, item);
// 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 => this.getSashPosition(sash) } : { getVerticalSashLeft: sash => this.getSashPosition(sash) };
const sash = new Sash(this.el, layoutProvider, { orientation });
const sashEventMapper = this.orientation === Orientation.VERTICAL
? (e: IBaseSashEvent) => ({ sash, start: e.startY, current: e.currentY })
: (e: IBaseSashEvent) => ({ sash, start: e.startX, current: e.currentX });
const onStart = mapEvent(fromEventEmitter<IBaseSashEvent>(sash, 'start'), sashEventMapper);
const onStartDisposable = onStart(this.onSashStart, this);
const onChange = mapEvent(fromEventEmitter<IBaseSashEvent>(sash, 'change'), sashEventMapper);
const onSashChangeDisposable = onChange(this.onSashChange, this);
const disposable = combinedDisposable([onStartDisposable, onSashChangeDisposable, sash]);
const sashItem: ISashItem = { sash, disposable };
this.sashItems.splice(index - 1, 0, sashItem);
}
view.render(container, this.orientation);
this.relayoutPreferredSizes();
}
removeView(index: number): void {
if (index < 0 || index >= this.viewItems.length) {
return;
}
// Remove view
const viewItem = this.viewItems.splice(index, 1)[0];
viewItem.disposable.dispose();
// Remove sash
if (this.viewItems.length >= 1) {
const sashIndex = Math.max(index - 1, 0);
const sashItem = this.sashItems.splice(sashIndex, 1)[0];
sashItem.disposable.dispose();
}
this.relayoutPreferredSizes();
}
moveView(from: number, to: number): void {
if (from < 0 || from >= this.viewItems.length) {
return;
}
if (to < 0 || to >= this.viewItems.length) {
return;
}
if (from === to) {
return;
}
const viewItem = this.viewItems.splice(from, 1)[0];
this.viewItems.splice(to, 0, viewItem);
this.layoutViews();
}
private relayoutPreferredSizes(): void {
this.viewItems.forEach(i => i.size = clamp(i.explicitSize, i.view.minimumSize, i.view.maximumSize));
this.relayout();
}
private relayout(): void {
const previousSize = this.size;
this.size = this.viewItems.reduce((r, i) => r + i.size, 0);
this.layout(previousSize);
}
layout(size: number): void {
this.resize(this.viewItems.length - 1, size - this.size);
this.size = Math.max(size, this.viewItems.reduce((r, i) => r + i.size, 0));
}
private onSashStart({ sash, start }: ISashEvent): void {
const index = firstIndex(this.sashItems, item => item.sash === sash);
const sizes = this.viewItems.map(i => i.size);
this.sashDragState = { start, index, sizes };
}
private onSashChange({ sash, current }: ISashEvent): void {
const { index, start, sizes } = this.sashDragState;
this.resize(index, current - start, sizes);
this.viewItems.forEach(viewItem => viewItem.explicitSize = viewItem.size);
}
private onViewChange(item: IViewItem): void {
item.size = clamp(item.size, item.view.minimumSize, item.view.maximumSize);
this.relayout();
}
resizeView(index: number, size: number): void {
if (index < 0 || index >= this.viewItems.length - 1) {
throw new Error('Cant resize view');
}
this.resize(index, size - this.viewItems[index].size);
}
private resize(index: number, delta: number, sizes = this.viewItems.map(i => i.size)): void {
if (index < 0 || index >= this.viewItems.length) {
return;
}
if (delta !== 0) {
const upIndexes = range(index, -1);
const up = upIndexes.map(i => this.viewItems[i]);
const upSizes = upIndexes.map(i => sizes[i]);
const downIndexes = range(index + 1, this.viewItems.length);
const down = downIndexes.map(i => this.viewItems[i]);
const downSizes = downIndexes.map(i => sizes[i]);
for (let i = 0, deltaUp = delta; deltaUp !== 0 && i < up.length; i++) {
const item = up[i];
const size = clamp(upSizes[i] + deltaUp, item.view.minimumSize, item.view.maximumSize);
const viewDelta = size - upSizes[i];
deltaUp -= viewDelta;
item.size = size;
}
for (let i = 0, deltaDown = delta; deltaDown !== 0 && i < down.length; i++) {
const item = down[i];
const size = clamp(downSizes[i] - deltaDown, item.view.minimumSize, item.view.maximumSize);
const viewDelta = size - downSizes[i];
deltaDown += viewDelta;
item.size = size;
}
}
this.layoutViews();
}
private layoutViews(): void {
this.viewItems.forEach(item => layoutViewItem(item, this.orientation));
this.sashItems.forEach(item => item.sash.layout());
// Update sashes enablement
let previous = false;
const collapsesDown = this.viewItems.map(i => previous = (i.size - i.view.minimumSize > 0) || previous);
previous = false;
const expandsDown = this.viewItems.map(i => previous = (i.view.maximumSize - i.size > 0) || previous);
const reverseViews = [...this.viewItems].reverse();
previous = false;
const collapsesUp = reverseViews.map(i => previous = (i.size - i.view.minimumSize > 0) || previous).reverse();
previous = false;
const expandsUp = reverseViews.map(i => previous = (i.view.maximumSize - i.size > 0) || previous).reverse();
this.sashItems.forEach((s, i) => {
if ((collapsesDown[i] && expandsUp[i + 1]) || (expandsDown[i] && collapsesUp[i + 1])) {
s.sash.enable();
} else {
s.sash.disable();
}
});
}
private getSashPosition(sash: Sash): number {
let position = 0;
for (let i = 0; i < this.sashItems.length; i++) {
position += this.viewItems[i].size;
if (this.sashItems[i].sash === sash) {
return position;
}
}
throw new Error('Sash not found');
}
dispose(): void {
this.viewItems.forEach(i => i.disposable.dispose());
this.viewItems = [];
this.sashItems.forEach(i => i.disposable.dispose());
this.sashItems = [];
}
}

View File

@@ -325,11 +325,43 @@ export function flatten<T>(arr: T[][]): T[] {
return arr.reduce((r, v) => r.concat(v), []);
}
export function range(to: number, from = 0): number[] {
export function range(to: number): number[];
export function range(from: number, to: number): number[];
export function range(arg: number, to?: number): number[] {
let from = typeof to === 'number' ? arg : 0;
if (typeof to === 'number') {
from = arg;
} else {
from = 0;
to = arg;
}
const result: number[] = [];
for (let i = from; i < to; i++) {
result.push(i);
if (from <= to) {
for (let i = from; i < to; i++) {
result.push(i);
}
} else {
for (let i = from; i > to; i--) {
result.push(i);
}
}
return result;
}
export function weave<T>(a: T[], b: T[]): T[] {
const result: T[] = [];
let ai = 0, bi = 0;
for (let i = 0, length = a.length + b.length; i < length; i++) {
if ((i % 2 === 0 && ai < a.length) || bi >= b.length) {
result.push(a[ai++]);
} else {
result.push(b[bi++]);
}
}
return result;

View File

@@ -45,6 +45,7 @@ export interface IJSONSchema {
patternErrorMessage?: string; // VSCode extension
deprecationMessage?: string; // VSCode extension
enumDescriptions?: string[]; // VSCode extension
doNotSuggest?: boolean; // VSCode extension
}
export interface IJSONSchemaMap {

View File

@@ -17,10 +17,10 @@ export interface ILabelProvider {
getLabel(element: any): string;
}
export interface IRootProvider {
getRoot(resource: URI): URI;
export interface IWorkspaceFolderProvider {
getWorkspaceFolder(resource: URI): URI;
getWorkspace(): {
roots: URI[];
folders: URI[];
};
}
@@ -28,7 +28,7 @@ export interface IUserHomeProvider {
userHome: string;
}
export function getPathLabel(resource: URI | string, rootProvider?: IRootProvider, userHomeProvider?: IUserHomeProvider): string {
export function getPathLabel(resource: URI | string, rootProvider?: IWorkspaceFolderProvider, userHomeProvider?: IUserHomeProvider): string {
if (!resource) {
return null;
}
@@ -38,9 +38,9 @@ export function getPathLabel(resource: URI | string, rootProvider?: IRootProvide
}
// return early if we can resolve a relative path label from the root
const baseResource = rootProvider ? rootProvider.getRoot(resource) : null;
const baseResource = rootProvider ? rootProvider.getWorkspaceFolder(resource) : null;
if (baseResource) {
const hasMultipleRoots = rootProvider.getWorkspace().roots.length > 1;
const hasMultipleRoots = rootProvider.getWorkspace().folders.length > 1;
let pathLabel: string;
if (isEqual(baseResource.fsPath, resource.fsPath, !platform.isLinux /* ignorecase */)) {

View File

@@ -46,3 +46,8 @@ export function countToArray(fromOrTo: number, to?: number): number[] {
return result;
}
export function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}

View File

@@ -238,9 +238,19 @@ export function regExpLeadsToEndlessLoop(regexp: RegExp): boolean {
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize}
*/
export const canNormalize = typeof ((<any>'').normalize) === 'function';
const nonAsciiCharactersPattern = /[^\u0000-\u0080]/;
const normalizedCache = new BoundedMap<string>(10000); // bounded to 10000 elements
const nfcCache = new BoundedMap<string>(10000); // bounded to 10000 elements
export function normalizeNFC(str: string): string {
return normalize(str, 'NFC', nfcCache);
}
const nfdCache = new BoundedMap<string>(10000); // bounded to 10000 elements
export function normalizeNFD(str: string): string {
return normalize(str, 'NFD', nfdCache);
}
const nonAsciiCharactersPattern = /[^\u0000-\u0080]/;
function normalize(str: string, form: string, normalizedCache: BoundedMap<string>): string {
if (!canNormalize || !str) {
return str;
}
@@ -252,7 +262,7 @@ export function normalizeNFC(str: string): string {
let res: string;
if (nonAsciiCharactersPattern.test(str)) {
res = (<any>str).normalize('NFC');
res = (<any>str).normalize(form);
} else {
res = str;
}

View File

@@ -301,8 +301,20 @@ export default class URI {
parts.push('//');
}
if (authority) {
let idx = authority.indexOf('@');
if (idx !== -1) {
const userinfo = authority.substr(0, idx);
authority = authority.substr(idx + 1);
idx = userinfo.indexOf(':');
if (idx === -1) {
parts.push(encoder(userinfo));
} else {
parts.push(encoder(userinfo.substr(0, idx)), ':', encoder(userinfo.substr(idx + 1)));
}
parts.push('@');
}
authority = authority.toLowerCase();
let idx = authority.indexOf(':');
idx = authority.indexOf(':');
if (idx === -1) {
parts.push(encoder(authority));
} else {

View File

@@ -6,7 +6,7 @@
'use strict';
import * as fs from 'fs';
import * as path from 'path';
import { dirname, basename } from 'path';
import * as objects from 'vs/base/common/objects';
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import Event, { Emitter } from 'vs/base/common/event';
@@ -49,9 +49,11 @@ export class ConfigWatcher<T> implements IConfigWatcher<T>, IDisposable {
private timeoutHandle: NodeJS.Timer;
private disposables: IDisposable[];
private _onDidUpdateConfiguration: Emitter<IConfigurationChangeEvent<T>>;
private configName: string;
constructor(private _path: string, private options: IConfigOptions<T> = { changeBufferDelay: 0, defaultConfig: Object.create(null), onError: error => console.error(error) }) {
this.disposables = [];
this.configName = basename(this._path);
this._onDidUpdateConfiguration = new Emitter<IConfigurationChangeEvent<T>>();
this.disposables.push(this._onDidUpdateConfiguration);
@@ -121,8 +123,8 @@ export class ConfigWatcher<T> implements IConfigWatcher<T>, IDisposable {
private registerWatcher(): void {
// Watch the parent of the path so that we detect ADD and DELETES
const parentFolder = path.dirname(this._path);
this.watch(parentFolder);
const parentFolder = dirname(this._path);
this.watch(parentFolder, true);
// Check if the path is a symlink and watch its target if so
fs.lstat(this._path, (err, stat) => {
@@ -137,20 +139,20 @@ export class ConfigWatcher<T> implements IConfigWatcher<T>, IDisposable {
return; // path is not a valid symlink
}
this.watch(realPath);
this.watch(realPath, false);
});
}
});
}
private watch(path: string): void {
private watch(path: string, isParentFolder: boolean): void {
if (this.disposed) {
return; // avoid watchers that will never get disposed by checking for being disposed
}
try {
const watcher = fs.watch(path);
watcher.on('change', () => this.onConfigFileChange());
watcher.on('change', (type, file) => this.onConfigFileChange(type, file.toString(), isParentFolder));
watcher.on('error', (code, signal) => this.options.onError(`Error watching ${path} for configuration changes (${code}, ${signal})`));
this.disposables.push(toDisposable(() => {
@@ -166,7 +168,11 @@ export class ConfigWatcher<T> implements IConfigWatcher<T>, IDisposable {
}
}
private onConfigFileChange(): void {
private onConfigFileChange(eventType: string, filename: string, isParentFolder: boolean): void {
if (isParentFolder && filename !== this.configName) {
return; // a change to a sibling file that is not our config file
}
if (this.timeoutHandle) {
global.clearTimeout(this.timeoutHandle);
this.timeoutHandle = null;

View File

@@ -189,3 +189,18 @@ const tmpDir = os.tmpdir();
export function del(path: string, tmp = tmpDir): TPromise<void> {
return nfcall(extfs.del, path, tmp);
}
export function whenDeleted(path: string): TPromise<void> {
// Complete when wait marker file is deleted
return new TPromise<void>(c => {
const interval = setInterval(() => {
fs.exists(path, exists => {
if (!exists) {
clearInterval(interval);
c(null);
}
});
}, 1000);
});
}

View File

@@ -0,0 +1,338 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Emitter } from 'vs/base/common/event';
import { SplitView, IView, Orientation } from 'vs/base/browser/ui/splitview/splitview2';
import { Sash } from 'vs/base/browser/ui/sash/sash';
class TestView implements IView {
private _onDidChange = new Emitter<void>();
readonly onDidChange = this._onDidChange.event;
get minimumSize(): number { return this._minimumSize; }
set minimumSize(size: number) { this._minimumSize = size; this._onDidChange.fire(); }
get maximumSize(): number { return this._maximumSize; }
set maximumSize(size: number) { this._maximumSize = size; this._onDidChange.fire(); }
private _onDidRender = new Emitter<{ container: HTMLElement; orientation: Orientation }>();
readonly onDidRender = this._onDidRender.event;
private _size = 0;
get size(): number { return this._size; }
private _onDidLayout = new Emitter<{ size: number; orientation: Orientation }>();
readonly onDidLayout = this._onDidLayout.event;
private _onDidFocus = new Emitter<void>();
readonly onDidFocus = this._onDidFocus.event;
constructor(
private _minimumSize: number,
private _maximumSize: number
) {
assert(_minimumSize <= _maximumSize, 'splitview view minimum size must be <= maximum size');
}
render(container: HTMLElement, orientation: Orientation): void {
this._onDidRender.fire({ container, orientation });
}
layout(size: number, orientation: Orientation): void {
this._size = size;
this._onDidLayout.fire({ size, orientation });
}
focus(): void {
this._onDidFocus.fire();
}
dispose(): void {
this._onDidChange.dispose();
this._onDidRender.dispose();
this._onDidLayout.dispose();
this._onDidFocus.dispose();
}
}
function getSashes(splitview: SplitView): Sash[] {
return (splitview as any).sashItems.map(i => i.sash) as Sash[];
}
suite('Splitview', () => {
let container: HTMLElement;
setup(() => {
container = document.createElement('div');
container.style.position = 'absolute';
container.style.width = `${200}px`;
container.style.height = `${200}px`;
});
teardown(() => {
container = null;
});
test('empty splitview has empty DOM', () => {
const splitview = new SplitView(container);
assert.equal(container.firstElementChild.childElementCount, 0, 'split view should be empty');
splitview.dispose();
});
test('has views as sashes as children', () => {
const view1 = new TestView(20, 20);
const view2 = new TestView(20, 20);
const view3 = new TestView(20, 20);
const splitview = new SplitView(container);
splitview.addView(view1, 20);
splitview.addView(view2, 20);
splitview.addView(view3, 20);
let viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view');
assert.equal(viewQuery.length, 3, 'split view should have 3 views');
let sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash');
assert.equal(sashQuery.length, 2, 'split view should have 2 sashes');
splitview.removeView(2);
viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view');
assert.equal(viewQuery.length, 2, 'split view should have 2 views');
sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash');
assert.equal(sashQuery.length, 1, 'split view should have 1 sash');
splitview.removeView(0);
viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view');
assert.equal(viewQuery.length, 1, 'split view should have 1 view');
sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash');
assert.equal(sashQuery.length, 0, 'split view should have no sashes');
splitview.removeView(0);
viewQuery = container.querySelectorAll('.monaco-split-view > .split-view-view');
assert.equal(viewQuery.length, 0, 'split view should have no views');
sashQuery = container.querySelectorAll('.monaco-split-view > .monaco-sash');
assert.equal(sashQuery.length, 0, 'split view should have no sashes');
splitview.dispose();
view1.dispose();
view2.dispose();
view3.dispose();
});
test('calls view methods on addView and removeView', () => {
const view = new TestView(20, 20);
const splitview = new SplitView(container);
let didLayout = false;
const layoutDisposable = view.onDidLayout(() => didLayout = true);
let didRender = false;
const renderDisposable = view.onDidRender(() => didRender = true);
splitview.addView(view, 20);
assert.equal(view.size, 20, 'view has right size');
assert(didLayout, 'layout is called');
assert(didLayout, 'render is called');
splitview.dispose();
layoutDisposable.dispose();
renderDisposable.dispose();
view.dispose();
});
test('stretches view to viewport', () => {
const view = new TestView(20, Number.POSITIVE_INFINITY);
const splitview = new SplitView(container);
splitview.layout(200);
splitview.addView(view, 20);
assert.equal(view.size, 200, 'view is stretched');
splitview.layout(200);
assert.equal(view.size, 200, 'view stayed the same');
splitview.layout(100);
assert.equal(view.size, 100, 'view is collapsed');
splitview.layout(20);
assert.equal(view.size, 20, 'view is collapsed');
splitview.layout(10);
assert.equal(view.size, 20, 'view is clamped');
splitview.layout(200);
assert.equal(view.size, 200, 'view is stretched');
splitview.dispose();
view.dispose();
});
test('respects preferred sizes with structural changes', () => {
const view1 = new TestView(20, Number.POSITIVE_INFINITY);
const view2 = new TestView(20, Number.POSITIVE_INFINITY);
const view3 = new TestView(20, Number.POSITIVE_INFINITY);
const splitview = new SplitView(container);
splitview.layout(200);
splitview.addView(view1, 20);
assert.equal(view1.size, 200, 'view1 is stretched');
splitview.addView(view2, 20);
assert.equal(view1.size, 20, 'view1 size is restored');
assert.equal(view2.size, 200 - 20, 'view2 is stretched');
splitview.addView(view3, 20);
assert.equal(view1.size, 20, 'view1 size is restored');
assert.equal(view2.size, 20, 'view2 size is restored');
assert.equal(view3.size, 160, 'view3 is stretched');
splitview.dispose();
view3.dispose();
view2.dispose();
view1.dispose();
});
test('can resize views', () => {
const view1 = new TestView(20, Number.POSITIVE_INFINITY);
const view2 = new TestView(20, Number.POSITIVE_INFINITY);
const view3 = new TestView(20, Number.POSITIVE_INFINITY);
const splitview = new SplitView(container);
splitview.layout(200);
splitview.addView(view1, 20);
splitview.addView(view2, 20);
splitview.addView(view3, 20);
assert.equal(view1.size, 20, 'view1 size is the default');
assert.equal(view2.size, 20, 'view2 size the the default');
assert.equal(view3.size, 160, 'view3 is stretched');
splitview.resizeView(1, 40);
assert.equal(view1.size, 20, 'view1 is untouched');
assert.equal(view2.size, 40, 'view2 is stretched');
assert.equal(view3.size, 140, 'view3 is collapsed');
splitview.resizeView(0, 70);
assert.equal(view1.size, 70, 'view1 is stretched');
assert.equal(view2.size, 20, 'view2 is collapsed');
assert.equal(view3.size, 110, 'view3 is collapsed');
assert.throws(() => splitview.resizeView(2, 20));
assert.equal(view1.size, 70, 'view1 stays the same');
assert.equal(view2.size, 20, 'view2 stays the same');
assert.equal(view3.size, 110, 'view3 stays the same');
splitview.dispose();
view3.dispose();
view2.dispose();
view1.dispose();
});
test('reacts to view changes', () => {
const view1 = new TestView(20, Number.POSITIVE_INFINITY);
const view2 = new TestView(20, Number.POSITIVE_INFINITY);
const view3 = new TestView(20, Number.POSITIVE_INFINITY);
const splitview = new SplitView(container);
splitview.layout(200);
splitview.addView(view1, 20);
splitview.addView(view2, 20);
splitview.addView(view3, 20);
assert.equal(view1.size, 20, 'view1 size is restored');
assert.equal(view2.size, 20, 'view2 size is restored');
assert.equal(view3.size, 160, 'view3 is stretched');
view3.maximumSize = 20;
assert.equal(view1.size, 20, 'view1 stays the same');
assert.equal(view2.size, 160, 'view2 is stretched');
assert.equal(view3.size, 20, 'view3 is collapsed');
view2.maximumSize = 40;
assert.equal(view1.size, 140, 'view1 is stretched');
assert.equal(view2.size, 40, 'view2 is collapsed');
assert.equal(view3.size, 20, 'view3 is collapsed');
view3.maximumSize = 200;
assert.equal(view1.size, 140, 'view1 stays the same');
assert.equal(view2.size, 40, 'view2 stays the same');
assert.equal(view3.size, 20, 'view3 stays the same');
view3.minimumSize = 100;
assert.equal(view1.size, 80, 'view1 is collapsed');
assert.equal(view2.size, 20, 'view2 stays the same');
assert.equal(view3.size, 100, 'view3 is stretched');
splitview.dispose();
view3.dispose();
view2.dispose();
view1.dispose();
});
test('sashes are properly enabled/disabled', () => {
const view1 = new TestView(20, Number.POSITIVE_INFINITY);
const view2 = new TestView(20, Number.POSITIVE_INFINITY);
const view3 = new TestView(20, Number.POSITIVE_INFINITY);
const splitview = new SplitView(container);
splitview.layout(200);
splitview.addView(view1, 20);
splitview.addView(view2, 20);
splitview.addView(view3, 20);
let sashes = getSashes(splitview);
assert.equal(sashes.length, 2, 'there are two sashes');
assert.equal(sashes[0].enabled, true, 'first sash is enabled');
assert.equal(sashes[1].enabled, true, 'second sash is enabled');
splitview.layout(60);
assert.equal(sashes[0].enabled, false, 'first sash is disabled');
assert.equal(sashes[1].enabled, false, 'second sash is disabled');
splitview.layout(20);
assert.equal(sashes[0].enabled, false, 'first sash is disabled');
assert.equal(sashes[1].enabled, false, 'second sash is disabled');
splitview.layout(200);
assert.equal(sashes[0].enabled, true, 'first sash is enabled');
assert.equal(sashes[1].enabled, true, 'second sash is enabled');
view1.maximumSize = 20;
assert.equal(sashes[0].enabled, false, 'first sash is disabled');
assert.equal(sashes[1].enabled, true, 'second sash is enabled');
view2.maximumSize = 20;
assert.equal(sashes[0].enabled, false, 'first sash is disabled');
assert.equal(sashes[1].enabled, false, 'second sash is disabled');
view1.maximumSize = 300;
assert.equal(sashes[0].enabled, true, 'first sash is enabled');
assert.equal(sashes[1].enabled, true, 'second sash is enabled');
view2.maximumSize = 200;
assert.equal(sashes[0].enabled, true, 'first sash is enabled');
assert.equal(sashes[1].enabled, true, 'second sash is enabled');
splitview.dispose();
view3.dispose();
view2.dispose();
view1.dispose();
});
});

View File

@@ -368,6 +368,23 @@ suite('URI', () => {
assert.equal(value.toString(), 'http://l%C3%B6calhost:8080/far');
});
test('URI#toString, user information in authority', () => {
var value = URI.parse('http://foo:bar@localhost/far');
assert.equal(value.toString(), 'http://foo:bar@localhost/far');
value = URI.parse('http://foo@localhost/far');
assert.equal(value.toString(), 'http://foo@localhost/far');
value = URI.parse('http://foo:bAr@localhost:8080/far');
assert.equal(value.toString(), 'http://foo:bAr@localhost:8080/far');
value = URI.parse('http://foo@localhost:8080/far');
assert.equal(value.toString(), 'http://foo@localhost:8080/far');
value = URI.from({ scheme: 'http', authority: 'föö:bör@löcalhost:8080', path: '/far', query: undefined, fragment: undefined });
assert.equal(value.toString(), 'http://f%C3%B6%C3%B6:b%C3%B6r@l%C3%B6calhost:8080/far');
});
test('correctFileUriToFilePath2', () => {
var test = (input: string, expected: string) => {