Merge remote-tracking branch 'upstream/master' into rebornix/viewportevent

This commit is contained in:
Peng Lyu 2019-11-15 14:02:14 -08:00
commit 18f2fd24c4
35 changed files with 991 additions and 299 deletions

View file

@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.41.0",
"distro": "403ab44be562c63a0cde1969fd8f5b45ff51709c",
"distro": "e3f0dacfbf82a87e6a3e826d18282d02a67c25c7",
"author": {
"name": "Microsoft Corporation"
},
@ -38,6 +38,7 @@
"iconv-lite": "0.5.0",
"jschardet": "2.1.1",
"keytar": "^4.11.0",
"msal": "^1.1.3",
"native-is-elevated": "0.4.1",
"native-keymap": "2.0.0",
"native-watchdog": "1.2.0",

View file

@ -10,6 +10,7 @@
"https-proxy-agent": "^2.2.3",
"iconv-lite": "0.5.0",
"jschardet": "2.1.1",
"msal": "^1.1.3",
"native-watchdog": "1.2.0",
"node-pty": "^0.10.0-beta2",
"onigasm-umd": "^2.2.4",

View file

@ -2,6 +2,7 @@
"name": "vscode-web",
"version": "0.0.0",
"dependencies": {
"msal": "^1.1.3",
"onigasm-umd": "^2.2.4",
"semver-umd": "^5.5.3",
"vscode-textmate": "^4.3.0",

View file

@ -2,6 +2,13 @@
# yarn lockfile v1
msal@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/msal/-/msal-1.1.3.tgz#d8c501d513f5e52eaf000f20a245526fc1ecaa2a"
integrity sha512-cdShb+N1H3OyR1y46ij6OO7QzeqC6BxrbrNcouS4JBrr1+DnZ55TumxQKEzWmTXHvsbsuz5PCyXZl812Un8L9g==
dependencies:
tslib "^1.9.3"
nan@^2.14.0:
version "2.14.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
@ -24,6 +31,11 @@ semver-umd@^5.5.3:
resolved "https://registry.yarnpkg.com/semver-umd/-/semver-umd-5.5.3.tgz#b64d7a2d4f5a717b369d56e31940a38e47e34d1e"
integrity sha512-HOnQrn2iKnVe/xlqCTzMXQdvSz3rPbD0DmQXYuQ+oK1dpptGFfPghonQrx5JHl2O7EJwDqtQnjhE7ME23q6ngw==
tslib@^1.9.3:
version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
vscode-textmate@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-4.3.0.tgz#6e1f0f273d84148cfa1e9c7ed85bd16c974f9f61"

View file

@ -256,6 +256,13 @@ ms@2.0.0:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
msal@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/msal/-/msal-1.1.3.tgz#d8c501d513f5e52eaf000f20a245526fc1ecaa2a"
integrity sha512-cdShb+N1H3OyR1y46ij6OO7QzeqC6BxrbrNcouS4JBrr1+DnZ55TumxQKEzWmTXHvsbsuz5PCyXZl812Un8L9g==
dependencies:
tslib "^1.9.3"
nan@^2.10.0, nan@^2.14.0:
version "2.14.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
@ -364,6 +371,11 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
tslib@^1.9.3:
version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"

View file

@ -56,5 +56,5 @@ export const BrowserFeatures = {
})(),
touch: 'ontouchstart' in window || navigator.maxTouchPoints > 0 || window.navigator.msMaxTouchPoints > 0,
pointerEvents: browser.isSafari && window.PointerEvent && ('ontouchstart' in window || navigator.maxTouchPoints > 0 || window.navigator.msMaxTouchPoints > 0)
pointerEvents: window.PointerEvent && ('ontouchstart' in window || window.navigator.maxTouchPoints > 0 || navigator.maxTouchPoints > 0 || window.navigator.msMaxTouchPoints > 0)
};

View file

@ -267,6 +267,13 @@ export let addStandardDisposableListener: IAddStandardDisposableListenerSignatur
return addDisposableListener(node, type, wrapHandler, useCapture);
};
export function addDisposableGenericMouseDownListner(node: EventTarget, handler: (event: any) => void, useCapture?: boolean): IDisposable {
return addDisposableListener(node, platform.isIOS && BrowserFeatures.pointerEvents ? EventType.POINTER_DOWN : EventType.MOUSE_DOWN, handler, useCapture);
}
export function addDisposableGenericMouseUpListner(node: EventTarget, handler: (event: any) => void, useCapture?: boolean): IDisposable {
return addDisposableListener(node, platform.isIOS && BrowserFeatures.pointerEvents ? EventType.POINTER_UP : EventType.MOUSE_UP, handler, useCapture);
}
export function addDisposableNonBubblingMouseOutListener(node: Element, handler: (event: MouseEvent) => void): IDisposable {
return addDisposableListener(node, 'mouseout', (e: MouseEvent) => {
// Mouse out bubbles, so this is an attempt to ignore faux mouse outs coming from children elements

View file

@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import * as platform from 'vs/base/common/platform';
import { IframeUtils } from 'vs/base/browser/iframe';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
@ -85,8 +86,8 @@ export class GlobalMouseMoveMonitor<R> implements IDisposable {
this.onStopCallback = onStopCallback;
let windowChain = IframeUtils.getSameOriginWindowChain();
const mouseMove = BrowserFeatures.pointerEvents ? 'pointermove' : 'mousemove';
const mouseUp = BrowserFeatures.pointerEvents ? 'pointerup' : 'mouseup';
const mouseMove = platform.isIOS && BrowserFeatures.pointerEvents ? 'pointermove' : 'mousemove';
const mouseUp = platform.isIOS && BrowserFeatures.pointerEvents ? 'pointerup' : 'mouseup';
for (const element of windowChain) {
this.hooks.add(dom.addDisposableThrottledListener(element.window.document, mouseMove,
(data: R) => this.mouseMoveCallback!(data),

View file

@ -5,6 +5,7 @@
import 'vs/css!./contextview';
import * as DOM from 'vs/base/browser/dom';
import * as platform from 'vs/base/common/platform';
import { IDisposable, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { Range } from 'vs/base/common/range';
import { BrowserFeatures } from 'vs/base/browser/canIUse';
@ -179,7 +180,7 @@ export class ContextView extends Disposable {
return;
}
if (this.delegate!.canRelayout === false && !BrowserFeatures.pointerEvents) {
if (this.delegate!.canRelayout === false && !(platform.isIOS && BrowserFeatures.pointerEvents)) {
this.hide();
return;
}

View file

@ -8,6 +8,7 @@ import * as dom from 'vs/base/browser/dom';
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
import { IMatch } from 'vs/base/common/filters';
import { Disposable } from 'vs/base/common/lifecycle';
import { Range } from 'vs/base/common/range';
export interface IIconLabelCreationOptions {
supportHighlights?: boolean;
@ -24,6 +25,7 @@ export interface IIconLabelValueOptions {
matches?: IMatch[];
labelEscapeNewLines?: boolean;
descriptionMatches?: IMatch[];
readonly separator?: string;
}
class FastLabelNode {
@ -86,9 +88,10 @@ class FastLabelNode {
}
export class IconLabel extends Disposable {
private domNode: FastLabelNode;
private labelDescriptionContainer: FastLabelNode;
private labelNode: FastLabelNode | HighlightedLabel;
private descriptionContainer: FastLabelNode;
private nameNode: Label | LabelWithHighlights;
private descriptionNode: FastLabelNode | HighlightedLabel | undefined;
private descriptionNodeFactory: () => FastLabelNode | HighlightedLabel;
@ -97,18 +100,21 @@ export class IconLabel extends Disposable {
this.domNode = this._register(new FastLabelNode(dom.append(container, dom.$('.monaco-icon-label'))));
this.labelDescriptionContainer = this._register(new FastLabelNode(dom.append(this.domNode.element, dom.$('.monaco-icon-label-description-container'))));
const labelContainer = dom.append(this.domNode.element, dom.$('.monaco-icon-label-container'));
const nameContainer = dom.append(labelContainer, dom.$('span.monaco-icon-name-container'));
this.descriptionContainer = this._register(new FastLabelNode(dom.append(labelContainer, dom.$('span.monaco-icon-description-container'))));
if (options?.supportHighlights) {
this.labelNode = new HighlightedLabel(dom.append(this.labelDescriptionContainer.element, dom.$('a.label-name')), !!options.supportCodicons);
this.nameNode = new LabelWithHighlights(nameContainer, !!options.supportCodicons);
} else {
this.labelNode = this._register(new FastLabelNode(dom.append(this.labelDescriptionContainer.element, dom.$('a.label-name'))));
this.nameNode = new Label(nameContainer);
}
if (options?.supportDescriptionHighlights) {
this.descriptionNodeFactory = () => new HighlightedLabel(dom.append(this.labelDescriptionContainer.element, dom.$('span.label-description')), !!options.supportCodicons);
this.descriptionNodeFactory = () => new HighlightedLabel(dom.append(this.descriptionContainer.element, dom.$('span.label-description')), !!options.supportCodicons);
} else {
this.descriptionNodeFactory = () => this._register(new FastLabelNode(dom.append(this.labelDescriptionContainer.element, dom.$('span.label-description'))));
this.descriptionNodeFactory = () => this._register(new FastLabelNode(dom.append(this.descriptionContainer.element, dom.$('span.label-description'))));
}
}
@ -116,7 +122,7 @@ export class IconLabel extends Disposable {
return this.domNode.element;
}
setLabel(label: string, description?: string, options?: IIconLabelValueOptions): void {
setLabel(label: string | string[], description?: string, options?: IIconLabelValueOptions): void {
const classes = ['monaco-icon-label'];
if (options) {
if (options.extraClasses) {
@ -131,11 +137,7 @@ export class IconLabel extends Disposable {
this.domNode.className = classes.join(' ');
this.domNode.title = options?.title || '';
if (this.labelNode instanceof HighlightedLabel) {
this.labelNode.set(label || '', options?.matches, options?.title, options?.labelEscapeNewLines);
} else {
this.labelNode.textContent = label || '';
}
this.nameNode.setLabel(label, options);
if (description || this.descriptionNode) {
if (!this.descriptionNode) {
@ -157,3 +159,110 @@ export class IconLabel extends Disposable {
}
}
}
class Label {
private label: string | string[] | undefined = undefined;
private singleLabel: HTMLElement | undefined = undefined;
constructor(private container: HTMLElement) { }
setLabel(label: string | string[], options?: IIconLabelValueOptions): void {
if (this.label === label) {
return;
}
this.label = label;
if (typeof label === 'string') {
if (!this.singleLabel) {
this.container.innerHTML = '';
dom.removeClass(this.container, 'multiple');
this.singleLabel = dom.append(this.container, dom.$('a.label-name'));
}
this.singleLabel.textContent = label;
} else {
this.container.innerHTML = '';
dom.addClass(this.container, 'multiple');
this.singleLabel = undefined;
for (let i = 0; i < label.length; i++) {
const l = label[i];
dom.append(this.container, dom.$('a.label-name', { 'data-icon-label-count': label.length, 'data-icon-label-index': i }, l));
if (i < label.length - 1) {
dom.append(this.container, dom.$('span.label-separator', undefined, options?.separator || '/'));
}
}
}
}
}
function splitMatches(labels: string[], separator: string, matches: IMatch[] | undefined): IMatch[][] | undefined {
if (!matches) {
return undefined;
}
let labelStart = 0;
return labels.map(label => {
const labelRange = { start: labelStart, end: labelStart + label.length };
const result = matches
.map(match => Range.intersect(labelRange, match))
.filter(range => !Range.isEmpty(range))
.map(({ start, end }) => ({ start: start - labelStart, end: end - labelStart }));
labelStart = labelRange.end + separator.length;
return result;
});
}
class LabelWithHighlights {
private label: string | string[] | undefined = undefined;
private singleLabel: HighlightedLabel | undefined = undefined;
constructor(private container: HTMLElement, private supportCodicons: boolean) { }
setLabel(label: string | string[], options?: IIconLabelValueOptions): void {
if (this.label === label) {
return;
}
this.label = label;
if (typeof label === 'string') {
if (!this.singleLabel) {
this.container.innerHTML = '';
dom.removeClass(this.container, 'multiple');
this.singleLabel = new HighlightedLabel(dom.append(this.container, dom.$('a.label-name')), this.supportCodicons);
}
this.singleLabel.set(label, options?.matches, options?.title, options?.labelEscapeNewLines);
} else {
this.container.innerHTML = '';
dom.addClass(this.container, 'multiple');
this.singleLabel = undefined;
const separator = options?.separator || '/';
const matches = splitMatches(label, separator, options?.matches);
for (let i = 0; i < label.length; i++) {
const l = label[i];
const m = matches ? matches[i] : undefined;
const name = dom.$('a.label-name', { 'data-icon-label-count': label.length, 'data-icon-label-index': i });
const highlightedLabel = new HighlightedLabel(dom.append(this.container, name), this.supportCodicons);
highlightedLabel.set(l, m, options?.title, options?.labelEscapeNewLines);
if (i < label.length - 1) {
dom.append(name, dom.$('span.label-separator', undefined, separator));
}
}
}
}
}

View file

@ -31,25 +31,33 @@
flex-shrink: 0; /* fix for https://github.com/Microsoft/vscode/issues/13787 */
}
.monaco-icon-label > .monaco-icon-label-description-container {
overflow: hidden; /* this causes the label/description to shrink first if decorations are enabled */
.monaco-icon-label > .monaco-icon-label-container {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.monaco-icon-label > .monaco-icon-label-description-container > .label-name {
.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-name-container > .label-name {
color: inherit;
white-space: pre; /* enable to show labels that include multiple whitespaces */
}
.monaco-icon-label > .monaco-icon-label-description-container > .label-description {
.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-name-container > .label-name > .label-separator {
margin-left: 3px;
margin-right: 1px;
opacity: 0.5;
}
.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
opacity: .7;
margin-left: 0.5em;
font-size: 0.9em;
white-space: pre; /* enable to show labels that include multiple whitespaces */
}
.monaco-icon-label.italic > .monaco-icon-label-description-container > .label-name,
.monaco-icon-label.italic > .monaco-icon-label-description-container > .label-description {
.monaco-icon-label.italic > .monaco-icon-label-container > .monaco-icon-name-container > .label-name,
.monaco-icon-label.italic > .monaco-icon-description-container > .label-description {
font-style: italic;
}
@ -58,7 +66,6 @@
font-size: 90%;
font-weight: 600;
padding: 0 16px 0 5px;
margin-left: auto;
text-align: center;
}

View file

@ -103,10 +103,11 @@ export const ListDragOverReactions = {
export interface IListDragAndDrop<T> {
getDragURI(element: T): string | null;
getDragLabel?(elements: T[]): string | undefined;
getDragLabel?(elements: T[], originalEvent: DragEvent): string | undefined;
onDragStart?(data: IDragAndDropData, originalEvent: DragEvent): void;
onDragOver(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | IListDragOverReaction;
drop(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void;
onDragEnd?(originalEvent: DragEvent): void;
}
export class ListError extends Error {

View file

@ -74,9 +74,10 @@ const DefaultOptions = {
horizontalScrolling: false
};
export class ElementsDragAndDropData<T> implements IDragAndDropData {
export class ElementsDragAndDropData<T, TContext = void> implements IDragAndDropData {
readonly elements: T[];
context: TContext | undefined;
constructor(elements: T[]) {
this.elements = elements;
@ -84,7 +85,7 @@ export class ElementsDragAndDropData<T> implements IDragAndDropData {
update(): void { }
getData(): any {
getData(): T[] {
return this.elements;
}
}
@ -99,7 +100,7 @@ export class ExternalElementsDragAndDropData<T> implements IDragAndDropData {
update(): void { }
getData(): any {
getData(): T[] {
return this.elements;
}
}
@ -766,7 +767,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
let label: string | undefined;
if (this.dnd.getDragLabel) {
label = this.dnd.getDragLabel(elements);
label = this.dnd.getDragLabel(elements, event);
}
if (typeof label === 'undefined') {
@ -846,10 +847,6 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
feedback = distinct(feedback).filter(i => i >= -1 && i < this.length).sort();
feedback = feedback[0] === -1 ? [-1] : feedback;
if (feedback.length === 0) {
throw new Error('Invalid empty feedback list');
}
if (equalsDragFeedback(this.currentDragFeedback, feedback)) {
return true;
}
@ -910,12 +907,16 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
this.dnd.drop(dragData, event.element, event.index, event.browserEvent);
}
private onDragEnd(): void {
private onDragEnd(event: DragEvent): void {
this.canDrop = false;
this.teardownDragAndDropScrollTopAnimation();
this.clearDragOverFeedback();
this.currentDragData = undefined;
StaticDND.CurrentDragAndDropData = undefined;
if (this.dnd.onDragEnd) {
this.dnd.onDragEnd(event);
}
}
private clearDragOverFeedback(): void {

View file

@ -1062,9 +1062,9 @@ class ListViewDragAndDrop<T> implements IListViewDragAndDrop<T> {
return this.dnd.getDragURI(element);
}
getDragLabel?(elements: T[]): string | undefined {
getDragLabel?(elements: T[], originalEvent: DragEvent): string | undefined {
if (this.dnd.getDragLabel) {
return this.dnd.getDragLabel(elements);
return this.dnd.getDragLabel(elements, originalEvent);
}
return undefined;
@ -1080,6 +1080,12 @@ class ListViewDragAndDrop<T> implements IListViewDragAndDrop<T> {
return this.dnd.onDragOver(data, targetElement, targetIndex, originalEvent);
}
onDragEnd(originalEvent: DragEvent): void {
if (this.dnd.onDragEnd) {
this.dnd.onDragEnd(originalEvent);
}
}
drop(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): void {
this.dnd.drop(data, targetElement, targetIndex, originalEvent);
}

View file

@ -27,10 +27,24 @@ import { clamp } from 'vs/base/common/numbers';
import { ScrollEvent } from 'vs/base/common/scrollable';
import { SetMap } from 'vs/base/common/collections';
class TreeElementsDragAndDropData<T, TFilterData, TContext> extends ElementsDragAndDropData<T, TContext> {
set context(context: TContext | undefined) {
this.data.context = context;
}
get context(): TContext | undefined {
return this.data.context;
}
constructor(private data: ElementsDragAndDropData<ITreeNode<T, TFilterData>, TContext>) {
super(data.elements.map(node => node.element));
}
}
function asTreeDragAndDropData<T, TFilterData>(data: IDragAndDropData): IDragAndDropData {
if (data instanceof ElementsDragAndDropData) {
const nodes = (data as ElementsDragAndDropData<ITreeNode<T, TFilterData>>).elements;
return new ElementsDragAndDropData(nodes.map(node => node.element));
return new TreeElementsDragAndDropData(data);
}
return data;
@ -47,9 +61,9 @@ class TreeNodeListDragAndDrop<T, TFilterData, TRef> implements IListDragAndDrop<
return this.dnd.getDragURI(node.element);
}
getDragLabel(nodes: ITreeNode<T, TFilterData>[]): string | undefined {
getDragLabel(nodes: ITreeNode<T, TFilterData>[], originalEvent: DragEvent): string | undefined {
if (this.dnd.getDragLabel) {
return this.dnd.getDragLabel(nodes.map(node => node.element));
return this.dnd.getDragLabel(nodes.map(node => node.element), originalEvent);
}
return undefined;
@ -87,7 +101,7 @@ class TreeNodeListDragAndDrop<T, TFilterData, TRef> implements IListDragAndDrop<
}, 500);
}
if (typeof result === 'boolean' || !result.accept || typeof result.bubble === 'undefined') {
if (typeof result === 'boolean' || !result.accept || typeof result.bubble === 'undefined' || result.feedback) {
if (!raw) {
const accept = typeof result === 'boolean' ? result : result.accept;
const effect = typeof result === 'boolean' ? undefined : result.effect;
@ -121,6 +135,12 @@ class TreeNodeListDragAndDrop<T, TFilterData, TRef> implements IListDragAndDrop<
this.dnd.drop(asTreeDragAndDropData(data), targetNode && targetNode.element, targetIndex, originalEvent);
}
onDragEnd(originalEvent: DragEvent): void {
if (this.dnd.onDragEnd) {
this.dnd.onDragEnd(originalEvent);
}
}
}
function asListOptions<T, TFilterData, TRef>(modelProvider: () => ITreeModel<T, TFilterData, TRef>, options?: IAbstractTreeOptions<T, TFilterData>): IListOptions<ITreeNode<T, TFilterData>> | undefined {

View file

@ -150,10 +150,24 @@ function asTreeContextMenuEvent<TInput, T>(e: ITreeContextMenuEvent<IAsyncDataTr
};
}
class AsyncDataTreeElementsDragAndDropData<TInput, T, TContext> extends ElementsDragAndDropData<T, TContext> {
set context(context: TContext | undefined) {
this.data.context = context;
}
get context(): TContext | undefined {
return this.data.context;
}
constructor(private data: ElementsDragAndDropData<IAsyncDataTreeNode<TInput, T>, TContext>) {
super(data.elements.map(node => node.element as T));
}
}
function asAsyncDataTreeDragAndDropData<TInput, T>(data: IDragAndDropData): IDragAndDropData {
if (data instanceof ElementsDragAndDropData) {
const nodes = (data as ElementsDragAndDropData<IAsyncDataTreeNode<TInput, T>>).elements;
return new ElementsDragAndDropData(nodes.map(node => node.element));
return new AsyncDataTreeElementsDragAndDropData(data);
}
return data;
@ -167,9 +181,9 @@ class AsyncDataTreeNodeListDragAndDrop<TInput, T> implements IListDragAndDrop<IA
return this.dnd.getDragURI(node.element as T);
}
getDragLabel(nodes: IAsyncDataTreeNode<TInput, T>[]): string | undefined {
getDragLabel(nodes: IAsyncDataTreeNode<TInput, T>[], originalEvent: DragEvent): string | undefined {
if (this.dnd.getDragLabel) {
return this.dnd.getDragLabel(nodes.map(node => node.element as T));
return this.dnd.getDragLabel(nodes.map(node => node.element as T), originalEvent);
}
return undefined;
@ -188,6 +202,12 @@ class AsyncDataTreeNodeListDragAndDrop<TInput, T> implements IListDragAndDrop<IA
drop(data: IDragAndDropData, targetNode: IAsyncDataTreeNode<TInput, T> | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
this.dnd.drop(asAsyncDataTreeDragAndDropData(data), targetNode && targetNode.element as T, targetIndex, originalEvent);
}
onDragEnd(originalEvent: DragEvent): void {
if (this.dnd.onDragEnd) {
this.dnd.onDragEnd(originalEvent);
}
}
}
function asObjectTreeOptions<TInput, T, TFilterData>(options?: IAsyncDataTreeOptions<T, TFilterData>): IObjectTreeOptions<IAsyncDataTreeNode<TInput, T>, TFilterData> | undefined {
@ -993,6 +1013,12 @@ class CompressibleAsyncDataTreeRenderer<TInput, T, TFilterData, TTemplateData> i
}
}
disposeCompressedElements(node: ITreeNode<ICompressedTreeNode<IAsyncDataTreeNode<TInput, T>>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>, height: number | undefined): void {
if (this.renderer.disposeCompressedElements) {
this.renderer.disposeCompressedElements(this.compressibleNodeMapperProvider().map(node) as ITreeNode<ICompressedTreeNode<T>, TFilterData>, index, templateData.templateData, height);
}
}
disposeTemplate(templateData: IDataTreeListTemplateData<TTemplateData>): void {
this.renderer.disposeTemplate(templateData.templateData);
}

View file

@ -10,6 +10,7 @@ let _isMacintosh = false;
let _isLinux = false;
let _isNative = false;
let _isWeb = false;
let _isIOS = false;
let _locale: string | undefined = undefined;
let _language: string = LANGUAGE_DEFAULT;
let _translationsConfigFile: string | undefined = undefined;
@ -41,6 +42,7 @@ declare const global: any;
interface INavigator {
userAgent: string;
language: string;
maxTouchPoints?: number;
}
declare const navigator: INavigator;
declare const self: any;
@ -52,6 +54,7 @@ if (typeof navigator === 'object' && !isElectronRenderer) {
_userAgent = navigator.userAgent;
_isWindows = _userAgent.indexOf('Windows') >= 0;
_isMacintosh = _userAgent.indexOf('Macintosh') >= 0;
_isIOS = _userAgent.indexOf('Macintosh') >= 0 && !!navigator.maxTouchPoints && navigator.maxTouchPoints > 0;
_isLinux = _userAgent.indexOf('Linux') >= 0;
_isWeb = true;
_locale = navigator.language;
@ -106,6 +109,7 @@ export const isMacintosh = _isMacintosh;
export const isLinux = _isLinux;
export const isNative = _isNative;
export const isWeb = _isWeb;
export const isIOS = _isIOS;
export const platform = _platform;
export const userAgent = _userAgent;

View file

@ -33,6 +33,7 @@
'xterm-addon-search': `${window.location.origin}/static/remote/web/node_modules/xterm-addon-search/lib/xterm-addon-search.js`,
'xterm-addon-web-links': `${window.location.origin}/static/remote/web/node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js`,
'semver-umd': `${window.location.origin}/static/remote/web/node_modules/semver-umd/lib/semver-umd.js`,
'Msal': `${window.location.origin}/static/node_modules/msal/dist/msal.min.js`,
}
};
</script>

View file

@ -37,6 +37,7 @@
'xterm-addon-search': `${window.location.origin}/static/node_modules/xterm-addon-search/lib/xterm-addon-search.js`,
'xterm-addon-web-links': `${window.location.origin}/static/node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js`,
'semver-umd': `${window.location.origin}/static/node_modules/semver-umd/lib/semver-umd.js`,
'Msal': `${window.location.origin}/static/node_modules/msal/dist/msal.min.js`,
}
};
</script>

View file

@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import * as platform from 'vs/base/common/platform';
import { EventType, Gesture, GestureEvent } from 'vs/base/browser/touch';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { IPointerHandlerHelper, MouseHandler, createMouseMoveEventMerger } from 'vs/editor/browser/controller/mouseHandler';
@ -283,7 +284,7 @@ export class PointerHandler extends Disposable {
super();
if (window.navigator.msPointerEnabled) {
this.handler = this._register(new MsPointerHandler(context, viewController, viewHelper));
} else if (((<any>window).PointerEvent && BrowserFeatures.pointerEvents)) {
} else if ((platform.isIOS && BrowserFeatures.pointerEvents)) {
this.handler = this._register(new PointerEventHandler(context, viewController, viewHelper));
} else if ((<any>window).TouchEvent) {
this.handler = this._register(new TouchHandler(context, viewController, viewHelper));

View file

@ -151,7 +151,7 @@ class SaturationBox extends Disposable {
this.layout();
this._register(dom.addDisposableListener(this.domNode, BrowserFeatures.pointerEvents ? dom.EventType.POINTER_DOWN : dom.EventType.MOUSE_DOWN, e => this.onMouseDown(e)));
this._register(dom.addDisposableGenericMouseDownListner(this.domNode, e => this.onMouseDown(e)));
this._register(this.model.onDidChangeColor(this.onDidChangeColor, this));
this.monitor = null;
}
@ -166,7 +166,7 @@ class SaturationBox extends Disposable {
this.monitor.startMonitoring(standardMouseMoveMerger, event => this.onDidChangePosition(event.posx - origin.left, event.posy - origin.top), () => null);
const mouseUpListener = dom.addDisposableListener(document, BrowserFeatures.pointerEvents ? dom.EventType.POINTER_UP : dom.EventType.MOUSE_UP, () => {
const mouseUpListener = dom.addDisposableGenericMouseUpListner(document, () => {
this._onColorFlushed.fire();
mouseUpListener.dispose();
if (this.monitor) {
@ -251,7 +251,7 @@ abstract class Strip extends Disposable {
this.slider = dom.append(this.domNode, $('.slider'));
this.slider.style.top = `0px`;
this._register(dom.addDisposableListener(this.domNode, BrowserFeatures.pointerEvents ? dom.EventType.POINTER_DOWN : dom.EventType.MOUSE_DOWN, e => this.onMouseDown(e)));
this._register(dom.addDisposableGenericMouseDownListner(this.domNode, e => this.onMouseDown(e)));
this.layout();
}
@ -273,7 +273,7 @@ abstract class Strip extends Disposable {
monitor.startMonitoring(standardMouseMoveMerger, event => this.onDidChangeTop(event.posy - origin.top), () => null);
const mouseUpListener = dom.addDisposableListener(document, BrowserFeatures.pointerEvents ? dom.EventType.POINTER_UP : dom.EventType.MOUSE_UP, () => {
const mouseUpListener = dom.addDisposableGenericMouseUpListner(document, () => {
this._onColorFlushed.fire();
mouseUpListener.dispose();
monitor.stopMonitoring(true);

View file

@ -28,7 +28,7 @@ import { withNullAsUndefined } from 'vs/base/common/types';
export interface IResourceLabelProps {
resource?: URI;
name?: string;
name?: string | string[];
description?: string;
}
@ -41,6 +41,7 @@ export interface IResourceLabelOptions extends IIconLabelValueOptions {
export interface IFileLabelOptions extends IResourceLabelOptions {
hideLabel?: boolean;
hidePath?: boolean;
readonly parentCount?: number;
}
export interface IResourceLabel extends IDisposable {
@ -442,7 +443,8 @@ class ResourceLabelWidget extends IconLabel {
title: '',
italic: this.options && this.options.italic,
matches: this.options && this.options.matches,
extraClasses: []
extraClasses: [],
separator: this.options?.separator
};
const resource = this.label.resource;

View file

@ -12,7 +12,7 @@ import { IAction, Action } from 'vs/base/common/actions';
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import * as DOM from 'vs/base/browser/dom';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { isMacintosh, isWeb } from 'vs/base/common/platform';
import { isMacintosh, isWeb, isIOS } from 'vs/base/common/platform';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
@ -674,7 +674,7 @@ export class CustomMenubarControl extends MenubarControl {
}
this._register(DOM.addDisposableListener(window, DOM.EventType.RESIZE, () => {
if (this.menubar && !BrowserFeatures.pointerEvents) {
if (this.menubar && !(isIOS && BrowserFeatures.pointerEvents)) {
this.menubar.blur();
}
}));

View file

@ -31,7 +31,6 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService
import { IModeService } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IListService, ListWidget } from 'vs/platform/list/browser/listService';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { Schemas } from 'vs/base/common/network';
import { IDialogService, IConfirmationResult, getConfirmMessage, IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
@ -40,7 +39,6 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
import { Constants } from 'vs/base/common/uint';
import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands';
import { coalesce } from 'vs/base/common/arrays';
import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree';
import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel';
import { onUnexpectedError, getErrorMessage } from 'vs/base/common/errors';
import { asDomUri, triggerDownload } from 'vs/base/browser/dom';
@ -840,22 +838,6 @@ class ClipboardContentProvider implements ITextModelContentProvider {
}
}
interface IExplorerContext {
stat?: ExplorerItem;
selection: ExplorerItem[];
}
function getContext(listWidget: ListWidget): IExplorerContext {
// These commands can only be triggered when explorer viewlet is visible so get it using the active viewlet
const tree = <AsyncDataTree<null, ExplorerItem>>listWidget;
const focus = tree.getFocus();
const stat = focus.length ? focus[0] : undefined;
const selection = tree.getSelection();
// Only respect the selection if user clicked inside it (focus belongs to it)
return { stat, selection: selection && typeof stat !== 'undefined' && selection.indexOf(stat) >= 0 ? selection : [] };
}
function onErrorWithRetry(notificationService: INotificationService, error: any, retry: () => Promise<any>): void {
notificationService.prompt(Severity.Error, toErrorMessage(error, false),
[{
@ -866,7 +848,6 @@ function onErrorWithRetry(notificationService: INotificationService, error: any,
}
async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boolean): Promise<void> {
const listService = accessor.get(IListService);
const explorerService = accessor.get(IExplorerService);
const fileService = accessor.get(IFileService);
const textFileService = accessor.get(ITextFileService);
@ -876,47 +857,45 @@ async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boole
await viewletService.openViewlet(VIEWLET_ID, true);
const list = listService.lastFocusedList;
if (list) {
const { stat } = getContext(list);
let folder: ExplorerItem;
if (stat) {
folder = stat.isDirectory ? stat : (stat.parent || explorerService.roots[0]);
} else {
folder = explorerService.roots[0];
}
if (folder.isReadonly) {
throw new Error('Parent folder is readonly.');
}
const newStat = new NewExplorerItem(folder, isFolder);
await folder.fetchChildren(fileService, explorerService);
folder.addChild(newStat);
const onSuccess = (value: string): Promise<void> => {
const createPromise = isFolder ? fileService.createFolder(resources.joinPath(folder.resource, value)) : textFileService.create(resources.joinPath(folder.resource, value));
return createPromise.then(created => {
refreshIfSeparator(value, explorerService);
return isFolder ? explorerService.select(created.resource, true)
: editorService.openEditor({ resource: created.resource, options: { pinned: true } }).then(() => undefined);
}, error => {
onErrorWithRetry(notificationService, error, () => onSuccess(value));
});
};
explorerService.setEditable(newStat, {
validationMessage: value => validateFileName(newStat, value),
onFinish: (value, success) => {
folder.removeChild(newStat);
explorerService.setEditable(newStat, null);
if (success) {
onSuccess(value);
}
}
});
const stats = explorerService.getContext(false);
const stat = stats.length > 0 ? stats[0] : undefined;
let folder: ExplorerItem;
if (stat) {
folder = stat.isDirectory ? stat : (stat.parent || explorerService.roots[0]);
} else {
folder = explorerService.roots[0];
}
if (folder.isReadonly) {
throw new Error('Parent folder is readonly.');
}
const newStat = new NewExplorerItem(folder, isFolder);
await folder.fetchChildren(fileService, explorerService);
folder.addChild(newStat);
const onSuccess = (value: string): Promise<void> => {
const createPromise = isFolder ? fileService.createFolder(resources.joinPath(folder.resource, value)) : textFileService.create(resources.joinPath(folder.resource, value));
return createPromise.then(created => {
refreshIfSeparator(value, explorerService);
return isFolder ? explorerService.select(created.resource, true)
: editorService.openEditor({ resource: created.resource, options: { pinned: true } }).then(() => undefined);
}, error => {
onErrorWithRetry(notificationService, error, () => onSuccess(value));
});
};
explorerService.setEditable(newStat, {
validationMessage: value => validateFileName(newStat, value),
onFinish: (value, success) => {
folder.removeChild(newStat);
explorerService.setEditable(newStat, null);
if (success) {
onSuccess(value);
}
}
});
}
CommandsRegistry.registerCommand({
@ -934,14 +913,11 @@ CommandsRegistry.registerCommand({
});
export const renameHandler = (accessor: ServicesAccessor) => {
const listService = accessor.get(IListService);
const explorerService = accessor.get(IExplorerService);
const textFileService = accessor.get(ITextFileService);
if (!listService.lastFocusedList) {
return;
}
const { stat } = getContext(listService.lastFocusedList);
const stats = explorerService.getContext(false);
const stat = stats.length > 0 ? stats[0] : undefined;
if (!stat) {
return;
}
@ -962,51 +938,32 @@ export const renameHandler = (accessor: ServicesAccessor) => {
};
export const moveFileToTrashHandler = (accessor: ServicesAccessor) => {
const listService = accessor.get(IListService);
if (!listService.lastFocusedList) {
return Promise.resolve();
}
const explorerContext = getContext(listService.lastFocusedList);
const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat!];
const explorerService = accessor.get(IExplorerService);
const stats = explorerService.getContext(true);
return deleteFiles(accessor.get(ITextFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), accessor.get(IFileService), stats, true);
};
export const deleteFileHandler = (accessor: ServicesAccessor) => {
const listService = accessor.get(IListService);
if (!listService.lastFocusedList) {
return Promise.resolve();
}
const explorerContext = getContext(listService.lastFocusedList);
const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat!];
const explorerService = accessor.get(IExplorerService);
const stats = explorerService.getContext(true);
return deleteFiles(accessor.get(ITextFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), accessor.get(IFileService), stats, false);
};
let pasteShouldMove = false;
export const copyFileHandler = (accessor: ServicesAccessor) => {
const listService = accessor.get(IListService);
if (!listService.lastFocusedList) {
return;
}
const explorerContext = getContext(listService.lastFocusedList);
const explorerService = accessor.get(IExplorerService);
if (explorerContext.stat) {
const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat];
const stats = explorerService.getContext(true);
if (stats.length > 0) {
explorerService.setToCopy(stats, false);
pasteShouldMove = false;
}
};
export const cutFileHandler = (accessor: ServicesAccessor) => {
const listService = accessor.get(IListService);
if (!listService.lastFocusedList) {
return;
}
const explorerContext = getContext(listService.lastFocusedList);
const explorerService = accessor.get(IExplorerService);
if (explorerContext.stat) {
const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat];
const stats = explorerService.getContext(true);
if (stats.length > 0) {
explorerService.setToCopy(stats, true);
pasteShouldMove = true;
}
@ -1014,47 +971,41 @@ export const cutFileHandler = (accessor: ServicesAccessor) => {
export const DOWNLOAD_COMMAND_ID = 'explorer.download';
const downloadFileHandler = (accessor: ServicesAccessor) => {
const listService = accessor.get(IListService);
if (!listService.lastFocusedList) {
return;
}
const explorerContext = getContext(listService.lastFocusedList);
const fileService = accessor.get(IFileService);
const fileDialogService = accessor.get(IFileDialogService);
const explorerService = accessor.get(IExplorerService);
const stats = explorerService.getContext(true);
if (explorerContext.stat) {
const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat];
stats.forEach(async s => {
if (isWeb) {
if (!s.isDirectory) {
triggerDownload(asDomUri(s.resource), s.name);
}
} else {
let defaultUri = s.isDirectory ? fileDialogService.defaultFolderPath() : fileDialogService.defaultFilePath();
if (defaultUri && !s.isDirectory) {
defaultUri = resources.joinPath(defaultUri, s.name);
}
const destination = await fileDialogService.showSaveDialog({
availableFileSystems: [Schemas.file],
saveLabel: mnemonicButtonLabel(nls.localize('download', "Download")),
title: s.isDirectory ? nls.localize('downloadFolder', "Download Folder") : nls.localize('downloadFile', "Download File"),
defaultUri
});
if (destination) {
await fileService.copy(s.resource, destination);
}
stats.forEach(async s => {
if (isWeb) {
if (!s.isDirectory) {
triggerDownload(asDomUri(s.resource), s.name);
}
});
}
} else {
let defaultUri = s.isDirectory ? fileDialogService.defaultFolderPath() : fileDialogService.defaultFilePath();
if (defaultUri && !s.isDirectory) {
defaultUri = resources.joinPath(defaultUri, s.name);
}
const destination = await fileDialogService.showSaveDialog({
availableFileSystems: [Schemas.file],
saveLabel: mnemonicButtonLabel(nls.localize('download', "Download")),
title: s.isDirectory ? nls.localize('downloadFolder', "Download Folder") : nls.localize('downloadFile', "Download File"),
defaultUri
});
if (destination) {
await fileService.copy(s.resource, destination);
}
}
});
};
CommandsRegistry.registerCommand({
id: DOWNLOAD_COMMAND_ID,
handler: downloadFileHandler
});
export const pasteFileHandler = async (accessor: ServicesAccessor) => {
const listService = accessor.get(IListService);
const clipboardService = accessor.get(IClipboardService);
const explorerService = accessor.get(IExplorerService);
const fileService = accessor.get(IFileService);
@ -1063,72 +1014,65 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => {
const editorService = accessor.get(IEditorService);
const configurationService = accessor.get(IConfigurationService);
if (listService.lastFocusedList) {
const explorerContext = getContext(listService.lastFocusedList);
const toPaste = resources.distinctParents(clipboardService.readResources(), r => r);
const element = explorerContext.stat || explorerService.roots[0];
const context = explorerService.getContext(true);
const toPaste = resources.distinctParents(clipboardService.readResources(), r => r);
const element = context.length ? context[0] : explorerService.roots[0];
// Check if target is ancestor of pasted folder
const stats = await Promise.all(toPaste.map(async fileToPaste => {
// Check if target is ancestor of pasted folder
const stats = await Promise.all(toPaste.map(async fileToPaste => {
if (element.resource.toString() !== fileToPaste.toString() && resources.isEqualOrParent(element.resource, fileToPaste)) {
throw new Error(nls.localize('fileIsAncestor', "File to paste is an ancestor of the destination folder"));
}
try {
const fileToPasteStat = await fileService.resolve(fileToPaste);
// Find target
let target: ExplorerItem;
if (element.resource.toString() === fileToPaste.toString()) {
target = element.parent!;
} else {
target = element.isDirectory ? element : element.parent!;
}
const incrementalNaming = configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
const targetFile = findValidPasteFileTarget(target, { resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwrite: pasteShouldMove }, incrementalNaming);
// Move/Copy File
if (pasteShouldMove) {
return await textFileService.move(fileToPaste, targetFile);
} else {
return await fileService.copy(fileToPaste, targetFile);
}
} catch (e) {
onError(notificationService, new Error(nls.localize('fileDeleted', "File to paste was deleted or moved meanwhile. {0}", getErrorMessage(e))));
return undefined;
}
}));
if (pasteShouldMove) {
// Cut is done. Make sure to clear cut state.
explorerService.setToCopy([], false);
if (element.resource.toString() !== fileToPaste.toString() && resources.isEqualOrParent(element.resource, fileToPaste)) {
throw new Error(nls.localize('fileIsAncestor', "File to paste is an ancestor of the destination folder"));
}
if (stats.length >= 1) {
const stat = stats[0];
if (stat && !stat.isDirectory && stats.length === 1) {
await editorService.openEditor({ resource: stat.resource, options: { pinned: true, preserveFocus: true } });
try {
const fileToPasteStat = await fileService.resolve(fileToPaste);
// Find target
let target: ExplorerItem;
if (element.resource.toString() === fileToPaste.toString()) {
target = element.parent!;
} else {
target = element.isDirectory ? element : element.parent!;
}
if (stat) {
await explorerService.select(stat.resource);
const incrementalNaming = configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
const targetFile = findValidPasteFileTarget(target, { resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwrite: pasteShouldMove }, incrementalNaming);
// Move/Copy File
if (pasteShouldMove) {
return await textFileService.move(fileToPaste, targetFile);
} else {
return await fileService.copy(fileToPaste, targetFile);
}
} catch (e) {
onError(notificationService, new Error(nls.localize('fileDeleted', "File to paste was deleted or moved meanwhile. {0}", getErrorMessage(e))));
return undefined;
}
}));
if (pasteShouldMove) {
// Cut is done. Make sure to clear cut state.
explorerService.setToCopy([], false);
}
if (stats.length >= 1) {
const stat = stats[0];
if (stat && !stat.isDirectory && stats.length === 1) {
await editorService.openEditor({ resource: stat.resource, options: { pinned: true, preserveFocus: true } });
}
if (stat) {
await explorerService.select(stat.resource);
}
}
};
export const openFilePreserveFocusHandler = async (accessor: ServicesAccessor) => {
const listService = accessor.get(IListService);
const editorService = accessor.get(IEditorService);
const explorerService = accessor.get(IExplorerService);
const stats = explorerService.getContext(true);
if (listService.lastFocusedList) {
const explorerContext = getContext(listService.lastFocusedList);
if (explorerContext.stat) {
const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat];
await editorService.openEditors(stats.filter(s => !s.isDirectory).map(s => ({
resource: s.resource,
options: { preserveFocus: true }
})));
}
}
await editorService.openEditors(stats.filter(s => !s.isDirectory).map(s => ({
resource: s.resource,
options: { preserveFocus: true }
})));
};

View file

@ -11,7 +11,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host';
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ExplorerFocusCondition, TextFileContentProvider, VIEWLET_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files';
import { ExplorerFocusCondition, TextFileContentProvider, VIEWLET_ID, IExplorerService, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, FilesExplorerFocusCondition } from 'vs/workbench/contrib/files/common/files';
import { ExplorerViewlet } from 'vs/workbench/contrib/files/browser/explorerViewlet';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
@ -19,7 +19,7 @@ import { ISaveOptions } from 'vs/workbench/services/workingCopy/common/workingCo
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { IListService } from 'vs/platform/list/browser/listService';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { RawContextKey, IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IResourceInput } from 'vs/platform/editor/common/editor';
import { IFileService } from 'vs/platform/files/common/files';
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
@ -80,6 +80,11 @@ export const ResourceSelectedForCompareContext = new RawContextKey<boolean>('res
export const REMOVE_ROOT_FOLDER_COMMAND_ID = 'removeRootFolder';
export const REMOVE_ROOT_FOLDER_LABEL = nls.localize('removeFolderFromWorkspace', "Remove Folder from Workspace");
export const PREVIOUS_COMPRESSED_FOLDER = 'previousCompressedFolder';
export const NEXT_COMPRESSED_FOLDER = 'nextCompressedFolder';
export const FIRST_COMPRESSED_FOLDER = 'firstCompressedFolder';
export const LAST_COMPRESSED_FOLDER = 'lastCompressedFolder';
export const openWindowCommand = (accessor: ServicesAccessor, toOpen: IWindowOpenable[], options?: IOpenWindowOptions) => {
if (Array.isArray(toOpen)) {
const hostService = accessor.get(IHostService);
@ -603,3 +608,81 @@ CommandsRegistry.registerCommand({
return workspaceEditingService.removeFolders(resources);
}
});
// Compressed item navigation
KeybindingsRegistry.registerCommandAndKeybindingRule({
weight: KeybindingWeight.WorkbenchContrib + 10,
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext.negate()),
primary: KeyCode.LeftArrow,
id: PREVIOUS_COMPRESSED_FOLDER,
handler: (accessor) => {
const viewletService = accessor.get(IViewletService);
const viewlet = viewletService.getActiveViewlet();
if (viewlet?.getId() !== VIEWLET_ID) {
return;
}
const explorer = viewlet as ExplorerViewlet;
const view = explorer.getExplorerView();
view.previousCompressedStat();
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
weight: KeybindingWeight.WorkbenchContrib + 10,
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerCompressedFocusContext, ExplorerCompressedLastFocusContext.negate()),
primary: KeyCode.RightArrow,
id: NEXT_COMPRESSED_FOLDER,
handler: (accessor) => {
const viewletService = accessor.get(IViewletService);
const viewlet = viewletService.getActiveViewlet();
if (viewlet?.getId() !== VIEWLET_ID) {
return;
}
const explorer = viewlet as ExplorerViewlet;
const view = explorer.getExplorerView();
view.nextCompressedStat();
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
weight: KeybindingWeight.WorkbenchContrib + 10,
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext.negate()),
primary: KeyCode.Home,
id: FIRST_COMPRESSED_FOLDER,
handler: (accessor) => {
const viewletService = accessor.get(IViewletService);
const viewlet = viewletService.getActiveViewlet();
if (viewlet?.getId() !== VIEWLET_ID) {
return;
}
const explorer = viewlet as ExplorerViewlet;
const view = explorer.getExplorerView();
view.firstCompressedStat();
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
weight: KeybindingWeight.WorkbenchContrib + 10,
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerCompressedFocusContext, ExplorerCompressedLastFocusContext.negate()),
primary: KeyCode.End,
id: LAST_COMPRESSED_FOLDER,
handler: (accessor) => {
const viewletService = accessor.get(IViewletService);
const viewlet = viewletService.getActiveViewlet();
if (viewlet?.getId() !== VIEWLET_ID) {
return;
}
const explorer = viewlet as ExplorerViewlet;
const view = explorer.getExplorerView();
view.lastCompressedStat();
}
});

View file

@ -128,6 +128,16 @@
line-height: normal;
}
.explorer-viewlet .explorer-item .monaco-icon-name-container.multiple > .label-name {
padding: 1px 1px 1px 2px;
border-radius: 3px;
}
.explorer-viewlet .explorer-item .monaco-icon-name-container.multiple > .label-name:hover > .monaco-highlighted-label,
.explorer-viewlet .monaco-list .monaco-list-row.focused .explorer-item .monaco-icon-name-container.multiple > .label-name.active > .monaco-highlighted-label {
text-decoration: underline;
}
.monaco-workbench.linux .explorer-viewlet .explorer-item .monaco-inputbox,
.monaco-workbench.mac .explorer-viewlet .explorer-item .monaco-inputbox {
height: 22px;

View file

@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri';
import * as perf from 'vs/base/common/performance';
import { IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions';
import { memoize } from 'vs/base/common/decorators';
import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService, ExplorerResourceCut, ExplorerResourceMoveableToTrash } from 'vs/workbench/contrib/files/common/files';
import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext } from 'vs/workbench/contrib/files/common/files';
import { NewFolderAction, NewFileAction, FileCopiedContext, RefreshExplorerView, CollapseExplorerView } from 'vs/workbench/contrib/files/browser/fileActions';
import { toResource, SideBySideEditor } from 'vs/workbench/common/editor';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
@ -29,7 +29,7 @@ import { DelayedDragHandler } from 'vs/base/browser/dnd';
import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { IViewletPanelOptions, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet';
import { ILabelService } from 'vs/platform/label/common/label';
import { ExplorerDelegate, ExplorerAccessibilityProvider, ExplorerDataSource, FilesRenderer, FilesFilter, FileSorter, FileDragAndDrop, ExplorerCompressionDelegate } from 'vs/workbench/contrib/files/browser/views/explorerViewer';
import { ExplorerDelegate, ExplorerAccessibilityProvider, ExplorerDataSource, FilesRenderer, ICompressedNavigationController, FilesFilter, FileSorter, FileDragAndDrop, ExplorerCompressionDelegate } from 'vs/workbench/contrib/files/browser/views/explorerViewer';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree';
@ -49,8 +49,19 @@ import { values } from 'vs/base/common/map';
import { first } from 'vs/base/common/arrays';
import { withNullAsUndefined } from 'vs/base/common/types';
import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
import { dispose } from 'vs/base/common/lifecycle';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import { attachStyler, IColorMapping } from 'vs/platform/theme/common/styler';
import { ColorValue, listDropBackground } from 'vs/platform/theme/common/colorRegistry';
import { Color } from 'vs/base/common/color';
interface IExplorerViewColors extends IColorMapping {
listDropBackground?: ColorValue | undefined;
}
interface IExplorerViewStyles {
listDropBackground?: Color;
}
export class ExplorerView extends ViewletPanel {
static readonly ID: string = 'workbench.explorer.fileView';
@ -65,6 +76,14 @@ export class ExplorerView extends ViewletPanel {
private rootContext: IContextKey<boolean>;
private resourceMoveableToTrash: IContextKey<boolean>;
private renderer!: FilesRenderer;
private styleElement!: HTMLStyleElement;
private compressedFocusContext: IContextKey<boolean>;
private compressedFocusFirstContext: IContextKey<boolean>;
private compressedFocusLastContext: IContextKey<boolean>;
private compressedNavigationController: ICompressedNavigationController | undefined;
// Refresh is needed on the initial explorer open
private shouldRefresh = true;
private dragHandler!: DelayedDragHandler;
@ -96,15 +115,20 @@ export class ExplorerView extends ViewletPanel {
this.resourceContext = instantiationService.createInstance(ResourceContextKey);
this._register(this.resourceContext);
this.folderContext = ExplorerFolderContext.bindTo(contextKeyService);
this.readonlyContext = ExplorerResourceReadonlyContext.bindTo(contextKeyService);
this.rootContext = ExplorerRootContext.bindTo(contextKeyService);
this.resourceMoveableToTrash = ExplorerResourceMoveableToTrash.bindTo(contextKeyService);
this.compressedFocusContext = ExplorerCompressedFocusContext.bindTo(contextKeyService);
this.compressedFocusFirstContext = ExplorerCompressedFirstFocusContext.bindTo(contextKeyService);
this.compressedFocusLastContext = ExplorerCompressedLastFocusContext.bindTo(contextKeyService);
this.explorerService.registerContextProvider(this);
const decorationProvider = new ExplorerDecorationsProvider(this.explorerService, contextService);
this._register(decorationService.registerDecorationsProvider(decorationProvider));
this._register(decorationProvider);
this._register(this.resourceContext);
}
get name(): string {
@ -161,6 +185,10 @@ export class ExplorerView extends ViewletPanel {
renderBody(container: HTMLElement): void {
const treeContainer = DOM.append(container, DOM.$('.explorer-folders-view'));
this.styleElement = DOM.createStyleSheet(treeContainer);
attachStyler<IExplorerViewColors>(this.themeService, { listDropBackground }, this.styleListDropBackground.bind(this));
this.createTree(treeContainer);
if (this.toolbar) {
@ -254,6 +282,39 @@ export class ExplorerView extends ViewletPanel {
}
}
getContext(respectMultiSelection: boolean): ExplorerItem[] {
let focusedStat: ExplorerItem | undefined;
if (this.compressedNavigationController) {
focusedStat = this.compressedNavigationController.current;
} else {
const focus = this.tree.getFocus();
focusedStat = focus.length ? focus[0] : undefined;
}
if (!focusedStat) {
return [];
}
const selectedStats: ExplorerItem[] = [];
for (const stat of this.tree.getSelection()) {
const controller = this.renderer.getCompressedNavigationController(stat);
if (controller) {
selectedStats.push(...controller.items);
} else {
selectedStats.push(stat);
}
}
if (respectMultiSelection && selectedStats.indexOf(focusedStat) >= 0) {
return selectedStats;
}
return [focusedStat];
}
private selectActiveFile(deselect?: boolean, reveal = this.autoReveal): void {
if (this.autoReveal) {
const activeFile = this.getActiveFile();
@ -278,14 +339,14 @@ export class ExplorerView extends ViewletPanel {
this._register(explorerLabels);
const updateWidth = (stat: ExplorerItem) => this.tree.updateWidth(stat);
const filesRenderer = this.instantiationService.createInstance(FilesRenderer, explorerLabels, updateWidth);
this._register(filesRenderer);
this.renderer = this.instantiationService.createInstance(FilesRenderer, explorerLabels, updateWidth);
this._register(this.renderer);
this._register(createFileIconThemableTreeContainerScope(container, this.themeService));
const isCompressionEnabled = () => this.configurationService.getValue<boolean>('explorer.compressSingleChildFolders');
this.tree = this.instantiationService.createInstance<typeof WorkbenchCompressibleAsyncDataTree, WorkbenchCompressibleAsyncDataTree<ExplorerItem | ExplorerItem[], ExplorerItem, FuzzyScore>>(WorkbenchCompressibleAsyncDataTree, 'FileExplorer', container, new ExplorerDelegate(), new ExplorerCompressionDelegate(), [filesRenderer],
this.tree = this.instantiationService.createInstance<typeof WorkbenchCompressibleAsyncDataTree, WorkbenchCompressibleAsyncDataTree<ExplorerItem | ExplorerItem[], ExplorerItem, FuzzyScore>>(WorkbenchCompressibleAsyncDataTree, 'FileExplorer', container, new ExplorerDelegate(), new ExplorerCompressionDelegate(), [this.renderer],
this.instantiationService.createInstance(ExplorerDataSource), {
compressionEnabled: isCompressionEnabled(),
accessibilityProvider: new ExplorerAccessibilityProvider(),
@ -391,7 +452,7 @@ export class ExplorerView extends ViewletPanel {
}
}
private setContextKeys(stat: ExplorerItem | null): void {
private setContextKeys(stat: ExplorerItem | null | undefined): void {
const isSingleFolder = this.contextService.getWorkbenchState() === WorkbenchState.FOLDER;
const resource = stat ? stat.resource : isSingleFolder ? this.contextService.getWorkspace().folders[0].uri : null;
this.resourceContext.set(resource);
@ -401,7 +462,18 @@ export class ExplorerView extends ViewletPanel {
}
private onContextMenu(e: ITreeContextMenuEvent<ExplorerItem>): void {
const stat = e.element;
const disposables = new DisposableStore();
let stat = e.element;
let anchor = e.anchor;
// Compressed folders
if (stat) {
const controller = this.renderer.getCompressedNavigationController(stat);
if (controller) {
anchor = controller.labels[controller.index];
}
}
// update dynamic contexts
this.fileCopiedContextKey.set(this.clipboardService.hasResources());
@ -412,17 +484,17 @@ export class ExplorerView extends ViewletPanel {
const actions: IAction[] = [];
const roots = this.explorerService.roots; // If the click is outside of the elements pass the root resource if there is only one root. If there are multiple roots pass empty object.
const arg = stat instanceof ExplorerItem ? stat.resource : roots.length === 1 ? roots[0].resource : {};
const actionsDisposable = createAndFillInContextMenuActions(this.contributedContextMenu, { arg, shouldForwardArgs: true }, actions, this.contextMenuService);
disposables.add(createAndFillInContextMenuActions(this.contributedContextMenu, { arg, shouldForwardArgs: true }, actions, this.contextMenuService));
this.contextMenuService.showContextMenu({
getAnchor: () => e.anchor,
getAnchor: () => anchor,
getActions: () => actions,
onHide: (wasCancelled?: boolean) => {
if (wasCancelled) {
this.tree.domFocus();
}
dispose(actionsDisposable);
disposables.dispose();
},
getActionsContext: () => stat && selection && selection.indexOf(stat) >= 0
? selection.map((fs: ExplorerItem) => fs.resource)
@ -431,7 +503,7 @@ export class ExplorerView extends ViewletPanel {
}
private onFocusChanged(elements: ExplorerItem[]): void {
const stat = elements && elements.length ? elements[0] : null;
const stat = elements && elements.length ? elements[0] : undefined;
this.setContextKeys(stat);
if (stat) {
@ -441,6 +513,17 @@ export class ExplorerView extends ViewletPanel {
} else {
this.resourceMoveableToTrash.reset();
}
this.compressedNavigationController = stat && this.renderer.getCompressedNavigationController(stat);
if (!this.compressedNavigationController) {
this.compressedFocusContext.set(false);
return;
}
this.compressedFocusContext.set(true);
// this.compressedNavigationController.last();
this.updateCompressedNavigationContextKeys(this.compressedNavigationController);
}
// General methods
@ -589,6 +672,60 @@ export class ExplorerView extends ViewletPanel {
this.tree.collapseAll();
}
previousCompressedStat(): void {
if (!this.compressedNavigationController) {
return;
}
this.compressedNavigationController.previous();
this.updateCompressedNavigationContextKeys(this.compressedNavigationController);
}
nextCompressedStat(): void {
if (!this.compressedNavigationController) {
return;
}
this.compressedNavigationController.next();
this.updateCompressedNavigationContextKeys(this.compressedNavigationController);
}
firstCompressedStat(): void {
if (!this.compressedNavigationController) {
return;
}
this.compressedNavigationController.first();
this.updateCompressedNavigationContextKeys(this.compressedNavigationController);
}
lastCompressedStat(): void {
if (!this.compressedNavigationController) {
return;
}
this.compressedNavigationController.last();
this.updateCompressedNavigationContextKeys(this.compressedNavigationController);
}
private updateCompressedNavigationContextKeys(controller: ICompressedNavigationController): void {
this.compressedFocusFirstContext.set(controller.index === 0);
this.compressedFocusLastContext.set(controller.index === controller.count - 1);
}
styleListDropBackground(styles: IExplorerViewStyles): void {
const content: string[] = [];
if (styles.listDropBackground) {
content.push(`.explorer-viewlet .explorer-item .monaco-icon-name-container.multiple > .label-name.drop-target { background-color: ${styles.listDropBackground}; }`);
}
const newStyles = content.join('\n');
if (newStyles !== this.styleElement.innerHTML) {
this.styleElement.innerHTML = newStyles;
}
}
dispose(): void {
if (this.dragHandler) {
this.dragHandler.dispose();

View file

@ -12,7 +12,7 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/
import { IFileService, FileKind, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IDisposable, Disposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import { IDisposable, Disposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { KeyCode } from 'vs/base/common/keyCodes';
import { IFileLabelOptions, IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
import { ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction, TreeDragOverBubble } from 'vs/base/browser/ui/tree/tree';
@ -51,6 +51,9 @@ import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'
import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree';
import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
import { VSBuffer } from 'vs/base/common/buffer';
import { ILabelService } from 'vs/platform/label/common/label';
import { isNumber } from 'vs/base/common/types';
import { domEvent } from 'vs/base/browser/event';
export class ExplorerDelegate implements IListVirtualDelegate<ExplorerItem> {
@ -114,6 +117,77 @@ export class ExplorerDataSource implements IAsyncDataSource<ExplorerItem | Explo
}
}
export interface ICompressedNavigationController {
readonly current: ExplorerItem;
readonly items: ExplorerItem[];
readonly labels: HTMLElement[];
readonly index: number;
readonly count: number;
previous(): void;
next(): void;
first(): void;
last(): void;
setIndex(index: number): void;
}
export class CompressedNavigationController implements ICompressedNavigationController {
private _index: number;
readonly labels: HTMLElement[];
get index(): number { return this._index; }
get count(): number { return this.items.length; }
get current(): ExplorerItem { return this.items[this._index]!; }
constructor(readonly items: ExplorerItem[], templateData: IFileTemplateData) {
this._index = items.length - 1;
this.labels = Array.from(templateData.container.querySelectorAll('.label-name')) as HTMLElement[];
DOM.addClass(this.labels[this._index], 'active');
}
previous(): void {
if (this._index <= 0) {
return;
}
this.setIndex(this._index - 1);
}
next(): void {
if (this._index >= this.items.length - 1) {
return;
}
this.setIndex(this._index + 1);
}
first(): void {
if (this._index === 0) {
return;
}
this.setIndex(0);
}
last(): void {
if (this._index === this.items.length - 1) {
return;
}
this.setIndex(this.items.length - 1);
}
setIndex(index: number): void {
if (index < 0 || index >= this.items.length) {
return;
}
DOM.removeClass(this.labels[this._index], 'active');
this._index = index;
DOM.addClass(this.labels[this._index], 'active');
}
}
export interface IFileTemplateData {
elementDisposable: IDisposable;
label: IResourceLabel;
@ -125,6 +199,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
private config: IFilesConfiguration;
private configListener: IDisposable;
private compressedNavigationControllers = new Map<ExplorerItem, CompressedNavigationController>();
constructor(
private labels: ResourceLabels,
@ -132,7 +207,8 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
@IContextViewService private readonly contextViewService: IContextViewService,
@IThemeService private readonly themeService: IThemeService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IExplorerService private readonly explorerService: IExplorerService
@IExplorerService private readonly explorerService: IExplorerService,
@ILabelService private readonly labelService: ILabelService
) {
this.config = this.configurationService.getValue<IFilesConfiguration>();
this.configListener = this.configurationService.onDidChangeConfiguration(e => {
@ -158,6 +234,8 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
const stat = node.element;
const editableData = this.explorerService.getEditableData(stat);
DOM.removeClass(templateData.label.element, 'compressed');
// File Label
if (!editableData) {
templateData.label.element.style.display = 'flex';
@ -175,23 +253,44 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
templateData.elementDisposable.dispose();
const stat = node.element.elements[node.element.elements.length - 1];
const label = node.element.elements.map(e => e.name).join('/');
const label = node.element.elements.map(e => e.name);
const editableData = this.explorerService.getEditableData(stat);
// File Label
if (!editableData) {
DOM.addClass(templateData.label.element, 'compressed');
templateData.label.element.style.display = 'flex';
templateData.elementDisposable = this.renderStat(stat, label, node.filterData, templateData);
const disposables = new DisposableStore();
disposables.add(this.renderStat(stat, label, node.filterData, templateData));
const compressedNavigationController = new CompressedNavigationController(node.element.elements, templateData);
this.compressedNavigationControllers.set(stat, compressedNavigationController);
domEvent(templateData.container, 'mousedown')(e => {
const result = getIconLabelNameFromHTMLElement(e.target);
if (result) {
compressedNavigationController.setIndex(result.index);
}
}, undefined, disposables);
disposables.add(toDisposable(() => {
this.compressedNavigationControllers.delete(stat);
}));
templateData.elementDisposable = disposables;
}
// Input Box
else {
DOM.removeClass(templateData.label.element, 'compressed');
templateData.label.element.style.display = 'none';
templateData.elementDisposable = this.renderInputBox(templateData.container, stat, editableData);
}
}
private renderStat(stat: ExplorerItem, label: string, filterData: FuzzyScore | undefined, templateData: IFileTemplateData): IDisposable {
private renderStat(stat: ExplorerItem, label: string | string[], filterData: FuzzyScore | undefined, templateData: IFileTemplateData): IDisposable {
templateData.label.element.style.display = 'flex';
const extraClasses = ['explorer-item'];
if (this.explorerService.isCut(stat)) {
@ -202,7 +301,8 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
fileKind: stat.isRoot ? FileKind.ROOT_FOLDER : stat.isDirectory ? FileKind.FOLDER : FileKind.FILE,
extraClasses,
fileDecorations: this.config.explorer.decorations,
matches: createMatches(filterData)
matches: createMatches(filterData),
separator: this.labelService.getSeparator(stat.resource.scheme, stat.resource.authority)
});
return templateData.label.onDidRender(() => {
@ -227,6 +327,9 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
label.setFile(joinPath(parent, value || ' '), labelOptions); // Use icon for ' ' if name is empty.
// hack: hide label
(label.element.firstElementChild as HTMLElement).style.display = 'none';
// Input field for name
const inputBox = new InputBox(label.element, this.contextViewService, {
validationOptions: {
@ -290,7 +393,11 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
});
}
disposeElement?(element: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
disposeElement(element: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
templateData.elementDisposable.dispose();
}
disposeCompressedElements(node: ITreeNode<ICompressedTreeNode<ExplorerItem>, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
templateData.elementDisposable.dispose();
}
@ -299,6 +406,10 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
templateData.label.dispose();
}
getCompressedNavigationController(stat: ExplorerItem): ICompressedNavigationController | undefined {
return this.compressedNavigationControllers.get(stat);
}
dispose(): void {
this.configListener.dispose();
}
@ -468,6 +579,9 @@ const fileOverwriteConfirm = (name: string) => {
export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop';
private compressedDragOverElement: HTMLElement | undefined;
private compressedDropTargetDisposable: IDisposable = Disposable.None;
private toDispose: IDisposable[];
private dropEnabled = false;
@ -498,6 +612,42 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
return false;
}
// Compressed folders
if (target) {
const compressedTarget = FileDragAndDrop.getCompressedStatFromDragEvent(target, originalEvent);
if (compressedTarget) {
const iconLabelName = getIconLabelNameFromHTMLElement(originalEvent.target);
if (iconLabelName && iconLabelName.index < iconLabelName.count - 1) {
const result = this._onDragOver(data, compressedTarget, targetIndex, originalEvent);
if (result) {
if (iconLabelName.element !== this.compressedDragOverElement) {
this.compressedDragOverElement = iconLabelName.element;
this.compressedDropTargetDisposable.dispose();
this.compressedDropTargetDisposable = toDisposable(() => {
DOM.removeClass(iconLabelName.element, 'drop-target');
this.compressedDragOverElement = undefined;
});
DOM.addClass(iconLabelName.element, 'drop-target');
}
return typeof result === 'boolean' ? result : { ...result, feedback: [] };
}
this.compressedDropTargetDisposable.dispose();
return false;
}
}
}
this.compressedDropTargetDisposable.dispose();
return this._onDragOver(data, target, targetIndex, originalEvent);
}
private _onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh));
const fromDesktop = data instanceof DesktopDragAndDropData;
const effect = (fromDesktop || isCopy) ? ListDragOverEffect.Copy : ListDragOverEffect.Move;
@ -516,7 +666,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
// In-Explorer DND
else {
const items = (data as ElementsDragAndDropData<ExplorerItem>).elements;
const items = FileDragAndDrop.getStatsFromDragAndDropData(data as ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>);
if (!target) {
// Dropping onto the empty area. Do not accept if items dragged are already
@ -591,16 +741,17 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
return element.resource.toString();
}
getDragLabel(elements: ExplorerItem[]): string | undefined {
if (elements.length > 1) {
return String(elements.length);
getDragLabel(elements: ExplorerItem[], originalEvent: DragEvent): string | undefined {
if (elements.length === 1) {
const stat = FileDragAndDrop.getCompressedStatFromDragEvent(elements[0], originalEvent);
return stat.name;
}
return elements[0].name;
return String(elements.length);
}
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
const items = (data as ElementsDragAndDropData<ExplorerItem>).elements;
const items = FileDragAndDrop.getStatsFromDragAndDropData(data as ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, originalEvent);
if (items && items.length && originalEvent.dataTransfer) {
// Apply some datatransfer types to allow for dragging the element outside of the application
this.instantiationService.invokeFunction(fillResourceDataTransfers, items, originalEvent);
@ -615,6 +766,17 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
}
drop(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
this.compressedDropTargetDisposable.dispose();
// Find compressed target
if (target) {
const compressedTarget = FileDragAndDrop.getCompressedStatFromDragEvent(target, originalEvent);
if (compressedTarget) {
target = compressedTarget;
}
}
// Find parent to add to
if (!target) {
target = this.explorerService.roots[this.explorerService.roots.length - 1];
@ -628,41 +790,43 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
// Desktop DND (Import file)
if (data instanceof DesktopDragAndDropData) {
this.handleExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
if (isWeb) {
this.handleWebExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
} else {
this.handleExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
}
}
// In-Explorer DND (Move/Copy file)
else {
this.handleExplorerDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
this.handleExplorerDrop(data as ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
}
}
private async handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
if (isWeb) {
data.files.forEach(file => {
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = async (event) => {
const name = file.name;
if (typeof name === 'string' && event.target?.result instanceof ArrayBuffer) {
if (target.getChild(name)) {
const { confirmed } = await this.dialogService.confirm(fileOverwriteConfirm(name));
if (!confirmed) {
return;
}
}
const resource = joinPath(target.resource, name);
await this.fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(event.target?.result)));
if (data.files.length === 1) {
await this.editorService.openEditor({ resource, options: { pinned: true } });
private async handleWebExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
data.files.forEach(file => {
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = async (event) => {
const name = file.name;
if (typeof name === 'string' && event.target?.result instanceof ArrayBuffer) {
if (target.getChild(name)) {
const { confirmed } = await this.dialogService.confirm(fileOverwriteConfirm(name));
if (!confirmed) {
return;
}
}
};
});
return;
}
const resource = joinPath(target.resource, name);
await this.fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(event.target?.result)));
if (data.files.length === 1) {
await this.editorService.openEditor({ resource, options: { pinned: true } });
}
}
};
});
}
private async handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
const droppedResources = extractResources(originalEvent, true);
// Check for dropped external files to be folders
const result = await this.fileService.resolveAll(droppedResources);
@ -755,8 +919,8 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
}
}
private async handleExplorerDrop(data: IDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
const elementsData = (data as ElementsDragAndDropData<ExplorerItem>).elements;
private async handleExplorerDrop(data: ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
const elementsData = FileDragAndDrop.getStatsFromDragAndDropData(data);
const items = distinctParents(elementsData, s => s.resource);
const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh);
@ -869,6 +1033,66 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
}
}
}
private static getStatsFromDragAndDropData(data: ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, dragStartEvent?: DragEvent): ExplorerItem[] {
if (data.context) {
return data.context;
}
// Detect compressed folder dragging
if (dragStartEvent && data.elements.length === 1) {
data.context = [FileDragAndDrop.getCompressedStatFromDragEvent(data.elements[0], dragStartEvent)];
return data.context;
}
return data.elements;
}
private static getCompressedStatFromDragEvent(stat: ExplorerItem, dragEvent: DragEvent): ExplorerItem {
const target = document.elementFromPoint(dragEvent.clientX, dragEvent.clientY);
const iconLabelName = getIconLabelNameFromHTMLElement(target);
if (iconLabelName) {
const { count, index } = iconLabelName;
let i = count - 1;
while (i > index && stat.parent) {
stat = stat.parent;
i--;
}
return stat;
}
return stat;
}
onDragEnd(): void {
this.compressedDropTargetDisposable.dispose();
}
}
function getIconLabelNameFromHTMLElement(target: HTMLElement | EventTarget | Element | null): { element: HTMLElement, count: number, index: number } | null {
if (!(target instanceof HTMLElement)) {
return null;
}
let element: HTMLElement | null = target;
while (element && !DOM.hasClass(element, 'monaco-list-row')) {
if (DOM.hasClass(element, 'label-name') && element.hasAttribute('data-icon-label-count')) {
const count = Number(element.getAttribute('data-icon-label-count'));
const index = Number(element.getAttribute('data-icon-label-index'));
if (isNumber(count) && isNumber(index)) {
return { element: element, count, index };
}
}
element = element.parentElement;
}
return null;
}
export class ExplorerCompressionDelegate implements ITreeCompressionDelegate<ExplorerItem> {

View file

@ -6,7 +6,7 @@
import { Event, Emitter } from 'vs/base/common/event';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IExplorerService, IEditableData, IFilesConfiguration, SortOrder, SortOrderConfiguration } from 'vs/workbench/contrib/files/common/files';
import { IExplorerService, IEditableData, IFilesConfiguration, SortOrder, SortOrderConfiguration, IContextProvider } from 'vs/workbench/contrib/files/common/files';
import { ExplorerItem, ExplorerModel } from 'vs/workbench/contrib/files/common/explorerModel';
import { URI } from 'vs/base/common/uri';
import { FileOperationEvent, FileOperation, IFileStat, IFileService, FileChangesEvent, FILES_EXCLUDE_CONFIG, FileChangeType, IResolveFileOptions } from 'vs/platform/files/common/files';
@ -41,6 +41,7 @@ export class ExplorerService implements IExplorerService {
private _sortOrder: SortOrder;
private cutItems: ExplorerItem[] | undefined;
private fileSystemProviderSchemes = new Set<string>();
private contextProvider: IContextProvider | undefined;
constructor(
@IFileService private fileService: IFileService,
@ -48,7 +49,7 @@ export class ExplorerService implements IExplorerService {
@IConfigurationService private configurationService: IConfigurationService,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IClipboardService private clipboardService: IClipboardService,
@IEditorService private editorService: IEditorService
@IEditorService private editorService: IEditorService,
) {
this._sortOrder = this.configurationService.getValue('explorer.sortOrder');
}
@ -81,6 +82,18 @@ export class ExplorerService implements IExplorerService {
return this._sortOrder;
}
registerContextProvider(contextProvider: IContextProvider): void {
this.contextProvider = contextProvider;
}
getContext(respectMultiSelection: boolean): ExplorerItem[] {
if (!this.contextProvider) {
return [];
}
return this.contextProvider.getContext(respectMultiSelection);
}
// Memoized locals
@memoize private get fileEventsFilter(): ResourceGlobMatcher {
const fileEventsFilter = this.instantiationService.createInstance(

View file

@ -51,6 +51,7 @@ export interface IExplorerService {
readonly onDidSelectResource: Event<{ resource?: URI, reveal?: boolean }>;
readonly onDidCopyItems: Event<{ items: ExplorerItem[], cut: boolean, previouslyCutItems: ExplorerItem[] | undefined }>;
getContext(respectMultiSelection: boolean): ExplorerItem[];
setEditable(stat: ExplorerItem, data: IEditableData | null): void;
getEditable(): { stat: ExplorerItem, data: IEditableData } | undefined;
getEditableData(stat: ExplorerItem): IEditableData | undefined;
@ -66,7 +67,14 @@ export interface IExplorerService {
* Will try to resolve the path in case the explorer is not yet expanded to the file yet.
*/
select(resource: URI, reveal?: boolean): Promise<void>;
registerContextProvider(contextProvider: IContextProvider): void;
}
export interface IContextProvider {
getContext(respectMultiSelection: boolean): ExplorerItem[];
}
export const IExplorerService = createDecorator<IExplorerService>('explorerService');
/**
@ -84,6 +92,11 @@ export const OpenEditorsVisibleContext = new RawContextKey<boolean>('openEditors
export const OpenEditorsFocusedContext = new RawContextKey<boolean>('openEditorsFocus', true);
export const ExplorerFocusedContext = new RawContextKey<boolean>('explorerViewletFocus', true);
// compressed nodes
export const ExplorerCompressedFocusContext = new RawContextKey<boolean>('explorerViewletCompressedFocus', true);
export const ExplorerCompressedFirstFocusContext = new RawContextKey<boolean>('explorerViewletCompressedFirstFocus', true);
export const ExplorerCompressedLastFocusContext = new RawContextKey<boolean>('explorerViewletCompressedLastFocus', true);
export const FilesExplorerFocusCondition = ContextKeyExpr.and(ExplorerViewletVisibleContext, FilesExplorerFocusedContext, ContextKeyExpr.not(InputFocusedContextKey));
export const ExplorerFocusCondition = ContextKeyExpr.and(ExplorerViewletVisibleContext, ExplorerFocusedContext, ContextKeyExpr.not(InputFocusedContextKey));

View file

@ -47,6 +47,7 @@ import { ISetting, ISettingsGroup, SettingValueType } from 'vs/workbench/service
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { isArray } from 'vs/base/common/types';
import { BrowserFeatures } from 'vs/base/browser/canIUse';
import { isIOS } from 'vs/base/common/platform';
const $ = DOM.$;
@ -911,7 +912,7 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre
const common = this.renderCommonTemplate(null, container, 'enum');
const selectBox = new SelectBox([], 0, this._contextViewService, undefined, {
useCustomDrawn: !BrowserFeatures.pointerEvents
useCustomDrawn: !(isIOS && BrowserFeatures.pointerEvents)
});
common.toDispose.push(selectBox);

View file

@ -135,6 +135,7 @@
.scm-viewlet .monaco-list .monaco-list-row .resource-group > .actions,
.scm-viewlet .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions {
display: none;
max-width: fit-content;
}
.scm-viewlet .monaco-list .monaco-list-row:hover .resource-group > .actions,

View file

@ -8,11 +8,15 @@ import { Event, Emitter } from 'vs/base/common/event';
import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth';
import { ICredentialsService } from 'vs/platform/credentials/common/credentials';
import { Disposable } from 'vs/base/common/lifecycle';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { URI } from 'vs/base/common/uri';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { UserAgentApplication } from 'Msal';
const SERVICE_NAME = 'VS Code';
const ACCOUNT = 'MyAccount';
const clientId = 'aebc6443-996d-45c2-90f0-388ff96faa56';
export class AuthTokenService extends Disposable implements IAuthTokenService {
_serviceBrand: undefined;
@ -24,15 +28,25 @@ export class AuthTokenService extends Disposable implements IAuthTokenService {
readonly _onDidGetCallback: Emitter<URI> = this._register(new Emitter<URI>());
private _msalInstance: UserAgentApplication | undefined;
private _loadMsal: Promise<void>;
constructor(
@ICredentialsService private readonly credentialsService: ICredentialsService,
@IQuickInputService private readonly quickInputService: IQuickInputService
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@INotificationService private readonly notificationService: INotificationService
) {
super();
this._status = AuthTokenStatus.Inactive;
this._loadMsal = (import('Msal')).then(msal => {
this._msalInstance = new msal.UserAgentApplication({ auth: { clientId } });
});
this.getToken().then(token => {
if (token) {
this.setStatus(AuthTokenStatus.Active);
} else {
this.setStatus(AuthTokenStatus.Inactive);
}
});
}
@ -41,16 +55,40 @@ export class AuthTokenService extends Disposable implements IAuthTokenService {
const token = await this.credentialsService.getPassword(SERVICE_NAME, ACCOUNT);
if (token) {
return token;
}
} else {
if (!this.environmentService.isBuilt) {
return;
}
return;
try {
await this._loadMsal;
const response = await this._msalInstance!.acquireTokenSilent({});
return response.accessToken;
} catch (e) {
return;
}
}
}
async login(): Promise<void> {
const token = await this.quickInputService.input({ placeHolder: localize('enter token', "Please provide the auth bearer token"), ignoreFocusLost: true, });
if (token) {
await this.credentialsService.setPassword(SERVICE_NAME, ACCOUNT, token);
// Cannot redirect to localhost in the implicit grant flow, fall back to asking for token when running out of sources
if (!this.environmentService.isBuilt) {
const token = await this.quickInputService.input({ placeHolder: localize('enter token', "Please provide the auth bearer token"), ignoreFocusLost: true, });
if (token) {
await this.credentialsService.setPassword(SERVICE_NAME, ACCOUNT, token);
this.setStatus(AuthTokenStatus.Active);
}
return;
}
try {
await this._loadMsal;
const response = await this._msalInstance!.loginPopup();
await this.credentialsService.setPassword(SERVICE_NAME, ACCOUNT, response.accessToken);
this.setStatus(AuthTokenStatus.Active);
} catch (e) {
this.notificationService.error(localize('loginFailed', "Login failed: {0}", e));
}
}

View file

@ -472,7 +472,8 @@
"**/vs/workbench/api/{common,browser}/**",
"**/vs/workbench/services/**/{common,browser}/**",
"vscode-textmate",
"onigasm-umd"
"onigasm-umd",
"Msal"
]
},
{

View file

@ -5649,6 +5649,13 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
msal@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/msal/-/msal-1.1.3.tgz#d8c501d513f5e52eaf000f20a245526fc1ecaa2a"
integrity sha512-cdShb+N1H3OyR1y46ij6OO7QzeqC6BxrbrNcouS4JBrr1+DnZ55TumxQKEzWmTXHvsbsuz5PCyXZl812Un8L9g==
dependencies:
tslib "^1.9.3"
multimatch@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-2.1.0.tgz#9c7906a22fb4c02919e2f5f75161b4cdbd4b2a2b"
@ -8504,6 +8511,11 @@ tslib@^1.8.1, tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
tslib@^1.9.3:
version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
tslint@^5.16.0:
version "5.16.0"
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.16.0.tgz#ae61f9c5a98d295b9a4f4553b1b1e831c1984d67"