mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-10 18:22:34 -05:00
Merge from vscode 718331d6f3ebd1b571530ab499edb266ddd493d5
This commit is contained in:
@@ -120,6 +120,6 @@ export const isWebKit = (userAgent.indexOf('AppleWebKit') >= 0);
|
||||
export const isChrome = (userAgent.indexOf('Chrome') >= 0);
|
||||
export const isSafari = (!isChrome && (userAgent.indexOf('Safari') >= 0));
|
||||
export const isWebkitWebView = (!isChrome && !isSafari && isWebKit);
|
||||
export const isIPad = (userAgent.indexOf('iPad') >= 0);
|
||||
export const isIPad = (userAgent.indexOf('iPad') >= 0 || (isSafari && navigator.maxTouchPoints > 0));
|
||||
export const isEdgeWebView = isEdge && (userAgent.indexOf('WebView/') >= 0);
|
||||
export const isStandalone = (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches);
|
||||
|
||||
@@ -1313,7 +1313,22 @@ export function asCSSUrl(uri: URI): string {
|
||||
return `url('${asDomUri(uri).toString(true).replace(/'/g, '%27')}')`;
|
||||
}
|
||||
|
||||
export function triggerDownload(uri: URI, name: string): void {
|
||||
|
||||
export function triggerDownload(dataOrUri: Uint8Array | URI, name: string): void {
|
||||
|
||||
// If the data is provided as Buffer, we create a
|
||||
// blog URL out of it to produce a valid link
|
||||
let url: string;
|
||||
if (URI.isUri(dataOrUri)) {
|
||||
url = dataOrUri.toString(true);
|
||||
} else {
|
||||
const blob = new Blob([dataOrUri]);
|
||||
url = URL.createObjectURL(blob);
|
||||
|
||||
// Ensure to free the data from DOM eventually
|
||||
setTimeout(() => URL.revokeObjectURL(url));
|
||||
}
|
||||
|
||||
// In order to download from the browser, the only way seems
|
||||
// to be creating a <a> element with download attribute that
|
||||
// points to the file to download.
|
||||
@@ -1321,7 +1336,7 @@ export function triggerDownload(uri: URI, name: string): void {
|
||||
const anchor = document.createElement('a');
|
||||
document.body.appendChild(anchor);
|
||||
anchor.download = name;
|
||||
anchor.href = uri.toString(true);
|
||||
anchor.href = url;
|
||||
anchor.click();
|
||||
|
||||
// Ensure to remove the element from DOM eventually
|
||||
|
||||
@@ -28,6 +28,7 @@ export class FastDomNode<T extends HTMLElement> {
|
||||
private _backgroundColor: string;
|
||||
private _layerHint: boolean;
|
||||
private _contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint';
|
||||
private _boxShadow: string;
|
||||
|
||||
constructor(domNode: T) {
|
||||
this.domNode = domNode;
|
||||
@@ -51,6 +52,7 @@ export class FastDomNode<T extends HTMLElement> {
|
||||
this._backgroundColor = '';
|
||||
this._layerHint = false;
|
||||
this._contain = 'none';
|
||||
this._boxShadow = '';
|
||||
}
|
||||
|
||||
public setMaxWidth(maxWidth: number): void {
|
||||
@@ -218,6 +220,14 @@ export class FastDomNode<T extends HTMLElement> {
|
||||
this.domNode.style.transform = this._layerHint ? 'translate3d(0px, 0px, 0px)' : '';
|
||||
}
|
||||
|
||||
public setBoxShadow(boxShadow: string): void {
|
||||
if (this._boxShadow === boxShadow) {
|
||||
return;
|
||||
}
|
||||
this._boxShadow = boxShadow;
|
||||
this.domNode.style.boxShadow = boxShadow;
|
||||
}
|
||||
|
||||
public setContain(contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint'): void {
|
||||
if (this._contain === contain) {
|
||||
return;
|
||||
|
||||
@@ -88,7 +88,7 @@ export class MenuBar extends Disposable {
|
||||
|
||||
private numMenusShown: number = 0;
|
||||
private menuStyle: IMenuStyles | undefined;
|
||||
private overflowLayoutScheduled: IDisposable | null = null;
|
||||
private overflowLayoutScheduled: IDisposable | undefined = undefined;
|
||||
|
||||
constructor(private container: HTMLElement, private options: IMenuBarOptions = {}) {
|
||||
super();
|
||||
@@ -419,9 +419,8 @@ export class MenuBar extends Disposable {
|
||||
DOM.removeNode(this.overflowMenu.titleElement);
|
||||
DOM.removeNode(this.overflowMenu.buttonElement);
|
||||
|
||||
if (this.overflowLayoutScheduled) {
|
||||
this.overflowLayoutScheduled = dispose(this.overflowLayoutScheduled);
|
||||
}
|
||||
dispose(this.overflowLayoutScheduled);
|
||||
this.overflowLayoutScheduled = undefined;
|
||||
}
|
||||
|
||||
blur(): void {
|
||||
@@ -561,7 +560,7 @@ export class MenuBar extends Disposable {
|
||||
if (!this.overflowLayoutScheduled) {
|
||||
this.overflowLayoutScheduled = DOM.scheduleAtNextAnimationFrame(() => {
|
||||
this.updateOverflowAction();
|
||||
this.overflowLayoutScheduled = null;
|
||||
this.overflowLayoutScheduled = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -287,6 +287,7 @@ export abstract class AbstractScrollableElement extends Widget {
|
||||
this._options.handleMouseWheel = massagedOptions.handleMouseWheel;
|
||||
this._options.mouseWheelScrollSensitivity = massagedOptions.mouseWheelScrollSensitivity;
|
||||
this._options.fastScrollSensitivity = massagedOptions.fastScrollSensitivity;
|
||||
this._options.scrollPredominantAxis = massagedOptions.scrollPredominantAxis;
|
||||
this._setListeningToMouseWheel(this._options.handleMouseWheel);
|
||||
|
||||
if (!this._options.lazyRender) {
|
||||
@@ -334,6 +335,14 @@ export abstract class AbstractScrollableElement extends Widget {
|
||||
let deltaY = e.deltaY * this._options.mouseWheelScrollSensitivity;
|
||||
let deltaX = e.deltaX * this._options.mouseWheelScrollSensitivity;
|
||||
|
||||
if (this._options.scrollPredominantAxis) {
|
||||
if (Math.abs(deltaY) >= Math.abs(deltaX)) {
|
||||
deltaX = 0;
|
||||
} else {
|
||||
deltaY = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._options.flipAxes) {
|
||||
[deltaY, deltaX] = [deltaX, deltaY];
|
||||
}
|
||||
@@ -553,6 +562,7 @@ function resolveOptions(opts: ScrollableElementCreationOptions): ScrollableEleme
|
||||
scrollYToX: (typeof opts.scrollYToX !== 'undefined' ? opts.scrollYToX : false),
|
||||
mouseWheelScrollSensitivity: (typeof opts.mouseWheelScrollSensitivity !== 'undefined' ? opts.mouseWheelScrollSensitivity : 1),
|
||||
fastScrollSensitivity: (typeof opts.fastScrollSensitivity !== 'undefined' ? opts.fastScrollSensitivity : 5),
|
||||
scrollPredominantAxis: (typeof opts.scrollPredominantAxis !== 'undefined' ? opts.scrollPredominantAxis : true),
|
||||
mouseWheelSmoothScroll: (typeof opts.mouseWheelSmoothScroll !== 'undefined' ? opts.mouseWheelSmoothScroll : true),
|
||||
arrowSize: (typeof opts.arrowSize !== 'undefined' ? opts.arrowSize : 11),
|
||||
|
||||
|
||||
@@ -55,6 +55,13 @@ export interface ScrollableElementCreationOptions {
|
||||
* Defaults to 5.
|
||||
*/
|
||||
fastScrollSensitivity?: number;
|
||||
/**
|
||||
* Whether the scrollable will only scroll along the predominant axis when scrolling both
|
||||
* vertically and horizontally at the same time.
|
||||
* Prevents horizontal drift when scrolling vertically on a trackpad.
|
||||
* Defaults to true.
|
||||
*/
|
||||
scrollPredominantAxis?: boolean;
|
||||
/**
|
||||
* Height for vertical arrows (top/bottom) and width for horizontal arrows (left/right).
|
||||
* Defaults to 11.
|
||||
@@ -113,6 +120,7 @@ export interface ScrollableElementChangeOptions {
|
||||
handleMouseWheel?: boolean;
|
||||
mouseWheelScrollSensitivity?: number;
|
||||
fastScrollSensitivity: number;
|
||||
scrollPredominantAxis: boolean;
|
||||
}
|
||||
|
||||
export interface ScrollableElementResolvedOptions {
|
||||
@@ -125,6 +133,7 @@ export interface ScrollableElementResolvedOptions {
|
||||
alwaysConsumeMouseWheel: boolean;
|
||||
mouseWheelScrollSensitivity: number;
|
||||
fastScrollSensitivity: number;
|
||||
scrollPredominantAxis: boolean;
|
||||
mouseWheelSmoothScroll: boolean;
|
||||
arrowSize: number;
|
||||
listenOnDomNode: HTMLElement | null;
|
||||
|
||||
@@ -1585,7 +1585,7 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
|
||||
}
|
||||
|
||||
open(elements: TRef[], browserEvent?: UIEvent): void {
|
||||
const indexes = elements.map(e => this.model.getListIndex(e));
|
||||
const indexes = elements.map(e => this.model.getListIndex(e)).filter(i => i >= 0);
|
||||
this.view.open(indexes, browserEvent);
|
||||
}
|
||||
|
||||
|
||||
@@ -562,6 +562,10 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
|
||||
const node = this.getDataNode(element);
|
||||
|
||||
if (this.tree.hasElement(node) && !this.tree.isCollapsible(node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.refreshPromise) {
|
||||
await this.root.refreshPromise;
|
||||
await Event.toPromise(this._onDidRender.event);
|
||||
|
||||
@@ -205,6 +205,10 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
|
||||
this.model.setChildren(node, children, _onDidCreateNode, _onDidDeleteNode);
|
||||
}
|
||||
|
||||
has(element: T | null): boolean {
|
||||
return this.nodes.has(element);
|
||||
}
|
||||
|
||||
getListIndex(location: T | null): number {
|
||||
const node = this.getCompressedNode(location);
|
||||
return this.model.getListIndex(node);
|
||||
@@ -421,6 +425,10 @@ export class CompressibleObjectTreeModel<T extends NonNullable<any>, TFilterData
|
||||
this.model.setCompressionEnabled(enabled);
|
||||
}
|
||||
|
||||
has(location: T | null): boolean {
|
||||
return this.model.has(location);
|
||||
}
|
||||
|
||||
getListIndex(location: T | null): number {
|
||||
return this.model.getListIndex(location);
|
||||
}
|
||||
|
||||
@@ -199,6 +199,10 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
|
||||
}
|
||||
}
|
||||
|
||||
has(location: number[]): boolean {
|
||||
return this.hasTreeNode(location);
|
||||
}
|
||||
|
||||
getListIndex(location: number[]): number {
|
||||
const { listIndex, visible, revealed } = this.getTreeNodeWithListIndex(location);
|
||||
return visible && revealed ? listIndex : -1;
|
||||
@@ -291,6 +295,8 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
|
||||
if (isCollapsibleStateUpdate(update)) {
|
||||
result = node.collapsible !== update.collapsible;
|
||||
node.collapsible = update.collapsible;
|
||||
} else if (!node.collapsible) {
|
||||
result = false;
|
||||
} else {
|
||||
result = node.collapsed !== update.collapsed;
|
||||
node.collapsed = update.collapsed;
|
||||
@@ -516,6 +522,21 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
|
||||
}
|
||||
}
|
||||
|
||||
// cheap
|
||||
private hasTreeNode(location: number[], node: IIndexTreeNode<T, TFilterData> = this.root): boolean {
|
||||
if (!location || location.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const [index, ...rest] = location;
|
||||
|
||||
if (index < 0 || index > node.children.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.hasTreeNode(rest, node.children[index]);
|
||||
}
|
||||
|
||||
// cheap
|
||||
private getTreeNode(location: number[], node: IIndexTreeNode<T, TFilterData> = this.root): IIndexTreeNode<T, TFilterData> {
|
||||
if (!location || location.length === 0) {
|
||||
|
||||
@@ -50,6 +50,10 @@ export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends
|
||||
this.model.resort(element, recursive);
|
||||
}
|
||||
|
||||
hasElement(element: T): boolean {
|
||||
return this.model.has(element);
|
||||
}
|
||||
|
||||
protected createModel(user: string, view: ISpliceable<ITreeNode<T, TFilterData>>, options: IObjectTreeOptions<T, TFilterData>): ITreeModel<T | null, TFilterData, T | null> {
|
||||
return new ObjectTreeModel(user, view, options);
|
||||
}
|
||||
|
||||
@@ -195,6 +195,10 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
|
||||
return this.model.getLastElementAncestor(location);
|
||||
}
|
||||
|
||||
has(element: T | null): boolean {
|
||||
return this.nodes.has(element);
|
||||
}
|
||||
|
||||
getListIndex(element: T | null): number {
|
||||
const location = this.getElementLocation(element);
|
||||
return this.model.getListIndex(location);
|
||||
|
||||
@@ -108,6 +108,8 @@ export interface ITreeModel<T, TFilterData, TRef> {
|
||||
readonly onDidChangeCollapseState: Event<ICollapseStateChangeEvent<T, TFilterData>>;
|
||||
readonly onDidChangeRenderNodeCount: Event<ITreeNode<T, TFilterData>>;
|
||||
|
||||
has(location: TRef): boolean;
|
||||
|
||||
getListIndex(location: TRef): number;
|
||||
getListRenderCount(location: TRef): number;
|
||||
getNode(location?: TRef): ITreeNode<T, any>;
|
||||
|
||||
@@ -584,3 +584,7 @@ export function mapArrayOrNot<T, U>(items: T | T[], fn: (_: T) => U): U | U[] {
|
||||
export function asArray<T>(x: T | T[]): T[] {
|
||||
return Array.isArray(x) ? x : [x];
|
||||
}
|
||||
|
||||
export function getRandomElement<T>(arr: T[]): T | undefined {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
@@ -175,7 +175,3 @@ export function applyEdits(text: string, edits: Edit[]): string {
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
export function isWS(text: string, offset: number) {
|
||||
return '\r\n \t'.indexOf(text.charAt(offset)) !== -1;
|
||||
}
|
||||
|
||||
@@ -91,6 +91,9 @@ export function toDisposable(fn: () => void): IDisposable {
|
||||
}
|
||||
|
||||
export class DisposableStore implements IDisposable {
|
||||
|
||||
static DISABLE_DISPOSED_WARNING = false;
|
||||
|
||||
private _toDispose = new Set<IDisposable>();
|
||||
private _isDisposed = false;
|
||||
|
||||
@@ -127,7 +130,9 @@ export class DisposableStore implements IDisposable {
|
||||
|
||||
markTracked(t);
|
||||
if (this._isDisposed) {
|
||||
console.warn(new Error('Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!').stack);
|
||||
if (!DisposableStore.DISABLE_DISPOSED_WARNING) {
|
||||
console.warn(new Error('Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!').stack);
|
||||
}
|
||||
} else {
|
||||
this._toDispose.add(t);
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ export function hasTrailingPathSeparator(resource: URI, sep: string = paths.sep)
|
||||
return fsp.length > extpath.getRoot(fsp).length && fsp[fsp.length - 1] === sep;
|
||||
} else {
|
||||
const p = resource.path;
|
||||
return p.length > 1 && p.charCodeAt(p.length - 1) === CharCode.Slash; // ignore the slash at offset 0
|
||||
return (p.length > 1 && p.charCodeAt(p.length - 1) === CharCode.Slash) && !(/^[a-zA-Z]:(\/$|\\$)/.test(resource.fsPath)); // ignore the slash at offset 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +191,7 @@ export function hasTrailingPathSeparator(resource: URI, sep: string = paths.sep)
|
||||
* Important: Doesn't remove the first slash, it would make the URI invalid
|
||||
*/
|
||||
export function removeTrailingPathSeparator(resource: URI, sep: string = paths.sep): URI {
|
||||
// Make sure that the path isn't a drive letter. A trailing separator there is not removable.
|
||||
if (hasTrailingPathSeparator(resource, sep)) {
|
||||
return resource.with({ path: resource.path.substr(0, resource.path.length - 1) });
|
||||
}
|
||||
@@ -226,7 +227,7 @@ export function relativePath(from: URI, to: URI, ignoreCase = hasToIgnoreCase(fr
|
||||
return undefined;
|
||||
}
|
||||
if (from.scheme === Schemas.file) {
|
||||
const relativePath = paths.relative(from.path, to.path);
|
||||
const relativePath = paths.relative(originalFSPath(from), originalFSPath(to));
|
||||
return isWindows ? extpath.toSlashes(relativePath) : relativePath;
|
||||
}
|
||||
let fromPath = from.path || '/', toPath = to.path || '/';
|
||||
|
||||
@@ -126,8 +126,8 @@ export class ConfigWatcher<T> extends Disposable implements IConfigWatcher<T> {
|
||||
}
|
||||
|
||||
private async handleSymbolicLink(): Promise<void> {
|
||||
const { stat, isSymbolicLink } = await statLink(this._path);
|
||||
if (isSymbolicLink && !stat.isDirectory()) {
|
||||
const { stat, symbolicLink } = await statLink(this._path);
|
||||
if (symbolicLink && !stat.isDirectory()) {
|
||||
const realPath = await realpath(this._path);
|
||||
|
||||
this.watch(realPath, false);
|
||||
|
||||
@@ -14,7 +14,18 @@ import { promisify } from 'util';
|
||||
import { isRootOrDriveLetter } from 'vs/base/common/extpath';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { normalizeNFC } from 'vs/base/common/normalization';
|
||||
import { encode, encodeStream } from 'vs/base/node/encoding';
|
||||
import { encode } from 'vs/base/node/encoding';
|
||||
|
||||
// See https://github.com/Microsoft/vscode/issues/30180
|
||||
const WIN32_MAX_FILE_SIZE = 300 * 1024 * 1024; // 300 MB
|
||||
const GENERAL_MAX_FILE_SIZE = 16 * 1024 * 1024 * 1024; // 16 GB
|
||||
|
||||
// See https://github.com/v8/v8/blob/5918a23a3d571b9625e5cce246bdd5b46ff7cd8b/src/heap/heap.cc#L149
|
||||
const WIN32_MAX_HEAP_SIZE = 700 * 1024 * 1024; // 700 MB
|
||||
const GENERAL_MAX_HEAP_SIZE = 700 * 2 * 1024 * 1024; // 1400 MB
|
||||
|
||||
export const MAX_FILE_SIZE = process.arch === 'ia32' ? WIN32_MAX_FILE_SIZE : GENERAL_MAX_FILE_SIZE;
|
||||
export const MAX_HEAP_SIZE = process.arch === 'ia32' ? WIN32_MAX_HEAP_SIZE : GENERAL_MAX_HEAP_SIZE;
|
||||
|
||||
export enum RimRafMode {
|
||||
|
||||
@@ -178,30 +189,52 @@ export function stat(path: string): Promise<fs.Stats> {
|
||||
}
|
||||
|
||||
export interface IStatAndLink {
|
||||
|
||||
// The stats of the file. If the file is a symbolic
|
||||
// link, the stats will be of that target file and
|
||||
// not the link itself.
|
||||
// If the file is a symbolic link pointing to a non
|
||||
// existing file, the stat will be of the link and
|
||||
// the `dangling` flag will indicate this.
|
||||
stat: fs.Stats;
|
||||
isSymbolicLink: boolean;
|
||||
|
||||
// Will be provided if the resource is a symbolic link
|
||||
// on disk. Use the `dangling` flag to find out if it
|
||||
// points to a resource that does not exist on disk.
|
||||
symbolicLink?: { dangling: boolean };
|
||||
}
|
||||
|
||||
export async function statLink(path: string): Promise<IStatAndLink> {
|
||||
|
||||
// First stat the link
|
||||
let linkStat: fs.Stats | undefined;
|
||||
let linkStatError: NodeJS.ErrnoException | undefined;
|
||||
let lstats: fs.Stats | undefined;
|
||||
try {
|
||||
linkStat = await lstat(path);
|
||||
lstats = await lstat(path);
|
||||
|
||||
// Return early if the stat is not a symbolic link at all
|
||||
if (!lstats.isSymbolicLink()) {
|
||||
return { stat: lstats };
|
||||
}
|
||||
} catch (error) {
|
||||
linkStatError = error;
|
||||
/* ignore - use stat() instead */
|
||||
}
|
||||
|
||||
// Then stat the target and return that
|
||||
const isLink = !!(linkStat && linkStat.isSymbolicLink());
|
||||
if (linkStatError || isLink) {
|
||||
const fileStat = await stat(path);
|
||||
// If the stat is a symbolic link or failed to stat, use fs.stat()
|
||||
// which for symbolic links will stat the target they point to
|
||||
try {
|
||||
const stats = await stat(path);
|
||||
|
||||
return { stat: fileStat, isSymbolicLink: isLink };
|
||||
return { stat: stats, symbolicLink: lstats?.isSymbolicLink() ? { dangling: false } : undefined };
|
||||
} catch (error) {
|
||||
|
||||
// If the link points to a non-existing file we still want
|
||||
// to return it as result while setting dangling: true flag
|
||||
if (error.code === 'ENOENT' && lstats) {
|
||||
return { stat: lstats, symbolicLink: { dangling: true } };
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { stat: linkStat!, isSymbolicLink: false };
|
||||
}
|
||||
|
||||
export function lstat(path: string): Promise<fs.Stats> {
|
||||
@@ -213,9 +246,7 @@ export function rename(oldPath: string, newPath: string): Promise<void> {
|
||||
}
|
||||
|
||||
export function renameIgnoreError(oldPath: string, newPath: string): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
fs.rename(oldPath, newPath, () => resolve());
|
||||
});
|
||||
return new Promise(resolve => fs.rename(oldPath, newPath, () => resolve()));
|
||||
}
|
||||
|
||||
export function unlink(path: string): Promise<void> {
|
||||
@@ -236,6 +267,10 @@ export function readFile(path: string, encoding?: string): Promise<Buffer | stri
|
||||
return promisify(fs.readFile)(path, encoding);
|
||||
}
|
||||
|
||||
export async function mkdirp(path: string, mode?: number): Promise<void> {
|
||||
return promisify(fs.mkdir)(path, { mode, recursive: true });
|
||||
}
|
||||
|
||||
// According to node.js docs (https://nodejs.org/docs/v6.5.0/api/fs.html#fs_fs_writefile_file_data_options_callback)
|
||||
// it is not safe to call writeFile() on the same path multiple times without waiting for the callback to return.
|
||||
// Therefor we use a Queue on the path that is given to us to sequentialize calls to the same path properly.
|
||||
@@ -244,12 +279,15 @@ const writeFilePathQueues: Map<string, Queue<void>> = new Map();
|
||||
export function writeFile(path: string, data: string, options?: IWriteFileOptions): Promise<void>;
|
||||
export function writeFile(path: string, data: Buffer, options?: IWriteFileOptions): Promise<void>;
|
||||
export function writeFile(path: string, data: Uint8Array, options?: IWriteFileOptions): Promise<void>;
|
||||
export function writeFile(path: string, data: NodeJS.ReadableStream, options?: IWriteFileOptions): Promise<void>;
|
||||
export function writeFile(path: string, data: string | Buffer | NodeJS.ReadableStream | Uint8Array, options?: IWriteFileOptions): Promise<void>;
|
||||
export function writeFile(path: string, data: string | Buffer | NodeJS.ReadableStream | Uint8Array, options?: IWriteFileOptions): Promise<void> {
|
||||
export function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise<void>;
|
||||
export function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise<void> {
|
||||
const queueKey = toQueueKey(path);
|
||||
|
||||
return ensureWriteFileQueue(queueKey).queue(() => writeFileAndFlush(path, data, options));
|
||||
return ensureWriteFileQueue(queueKey).queue(() => {
|
||||
const ensuredOptions = ensureWriteOptions(options);
|
||||
|
||||
return new Promise((resolve, reject) => doWriteFileAndFlush(path, data, ensuredOptions, error => error ? reject(error) : resolve()));
|
||||
});
|
||||
}
|
||||
|
||||
function toQueueKey(path: string): string {
|
||||
@@ -294,103 +332,6 @@ interface IEnsuredWriteFileOptions extends IWriteFileOptions {
|
||||
}
|
||||
|
||||
let canFlush = true;
|
||||
function writeFileAndFlush(path: string, data: string | Buffer | NodeJS.ReadableStream | Uint8Array, options: IWriteFileOptions | undefined): Promise<void> {
|
||||
const ensuredOptions = ensureWriteOptions(options);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof data === 'string' || Buffer.isBuffer(data) || data instanceof Uint8Array) {
|
||||
doWriteFileAndFlush(path, data, ensuredOptions, error => error ? reject(error) : resolve());
|
||||
} else {
|
||||
doWriteFileStreamAndFlush(path, data, ensuredOptions, error => error ? reject(error) : resolve());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function doWriteFileStreamAndFlush(path: string, reader: NodeJS.ReadableStream, options: IEnsuredWriteFileOptions, callback: (error?: Error) => void): void {
|
||||
|
||||
// finish only once
|
||||
let finished = false;
|
||||
const finish = (error?: Error) => {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
|
||||
// in error cases we need to manually close streams
|
||||
// if the write stream was successfully opened
|
||||
if (error) {
|
||||
if (isOpen) {
|
||||
writer.once('close', () => callback(error));
|
||||
writer.destroy();
|
||||
} else {
|
||||
callback(error);
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise just return without error
|
||||
else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// create writer to target. we set autoClose: false because we want to use the streams
|
||||
// file descriptor to call fs.fdatasync to ensure the data is flushed to disk
|
||||
const writer = fs.createWriteStream(path, { mode: options.mode, flags: options.flag, autoClose: false });
|
||||
|
||||
// Event: 'open'
|
||||
// Purpose: save the fd for later use and start piping
|
||||
// Notes: will not be called when there is an error opening the file descriptor!
|
||||
let fd: number;
|
||||
let isOpen: boolean;
|
||||
writer.once('open', descriptor => {
|
||||
fd = descriptor;
|
||||
isOpen = true;
|
||||
|
||||
// if an encoding is provided, we need to pipe the stream through
|
||||
// an encoder stream and forward the encoding related options
|
||||
if (options.encoding) {
|
||||
reader = reader.pipe(encodeStream(options.encoding.charset, { addBOM: options.encoding.addBOM }));
|
||||
}
|
||||
|
||||
// start data piping only when we got a successful open. this ensures that we do
|
||||
// not consume the stream when an error happens and helps to fix this issue:
|
||||
// https://github.com/Microsoft/vscode/issues/42542
|
||||
reader.pipe(writer);
|
||||
});
|
||||
|
||||
// Event: 'error'
|
||||
// Purpose: to return the error to the outside and to close the write stream (does not happen automatically)
|
||||
reader.once('error', error => finish(error));
|
||||
writer.once('error', error => finish(error));
|
||||
|
||||
// Event: 'finish'
|
||||
// Purpose: use fs.fdatasync to flush the contents to disk
|
||||
// Notes: event is called when the writer has finished writing to the underlying resource. we must call writer.close()
|
||||
// because we have created the WriteStream with autoClose: false
|
||||
writer.once('finish', () => {
|
||||
|
||||
// flush to disk
|
||||
if (canFlush && isOpen) {
|
||||
fs.fdatasync(fd, (syncError: Error) => {
|
||||
|
||||
// In some exotic setups it is well possible that node fails to sync
|
||||
// In that case we disable flushing and warn to the console
|
||||
if (syncError) {
|
||||
console.warn('[node.js fs] fdatasync is now disabled for this session because it failed: ', syncError);
|
||||
canFlush = false;
|
||||
}
|
||||
|
||||
writer.destroy();
|
||||
});
|
||||
} else {
|
||||
writer.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// Event: 'close'
|
||||
// Purpose: signal we are done to the outside
|
||||
// Notes: event is called when the writer's filedescriptor is closed
|
||||
writer.once('close', () => finish());
|
||||
}
|
||||
|
||||
// Calls fs.writeFile() followed by a fs.sync() call to flush the changes to disk
|
||||
// We do this in cases where we want to make sure the data is really on disk and
|
||||
@@ -631,18 +572,3 @@ async function doCopyFile(source: string, target: string, mode: number): Promise
|
||||
reader.pipe(writer);
|
||||
});
|
||||
}
|
||||
|
||||
export async function mkdirp(path: string, mode?: number): Promise<void> {
|
||||
return promisify(fs.mkdir)(path, { mode, recursive: true });
|
||||
}
|
||||
|
||||
// See https://github.com/Microsoft/vscode/issues/30180
|
||||
const WIN32_MAX_FILE_SIZE = 300 * 1024 * 1024; // 300 MB
|
||||
const GENERAL_MAX_FILE_SIZE = 16 * 1024 * 1024 * 1024; // 16 GB
|
||||
|
||||
// See https://github.com/v8/v8/blob/5918a23a3d571b9625e5cce246bdd5b46ff7cd8b/src/heap/heap.cc#L149
|
||||
const WIN32_MAX_HEAP_SIZE = 700 * 1024 * 1024; // 700 MB
|
||||
const GENERAL_MAX_HEAP_SIZE = 700 * 2 * 1024 * 1024; // 1400 MB
|
||||
|
||||
export const MAX_FILE_SIZE = process.arch === 'ia32' ? WIN32_MAX_FILE_SIZE : GENERAL_MAX_FILE_SIZE;
|
||||
export const MAX_HEAP_SIZE = process.arch === 'ia32' ? WIN32_MAX_HEAP_SIZE : GENERAL_MAX_HEAP_SIZE;
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, Emitter, Relay } from 'vs/base/common/event';
|
||||
import { IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter, Relay, EventMultiplexer } from 'vs/base/common/event';
|
||||
import { IDisposable, toDisposable, combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { getRandomElement } from 'vs/base/common/arrays';
|
||||
import { isFunction } from 'vs/base/common/types';
|
||||
|
||||
/**
|
||||
* An `IChannel` is an abstraction over a collection of commands.
|
||||
@@ -95,7 +97,8 @@ export interface Client<TContext> {
|
||||
|
||||
export interface IConnectionHub<TContext> {
|
||||
readonly connections: Connection<TContext>[];
|
||||
readonly onDidChangeConnections: Event<Connection<TContext>>;
|
||||
readonly onDidAddConnection: Event<Connection<TContext>>;
|
||||
readonly onDidRemoveConnection: Event<Connection<TContext>>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,7 +119,7 @@ export interface IClientRouter<TContext = string> {
|
||||
* order to pick the right one.
|
||||
*/
|
||||
export interface IRoutingChannelClient<TContext = string> {
|
||||
getChannel<T extends IChannel>(channelName: string, router: IClientRouter<TContext>): T;
|
||||
getChannel<T extends IChannel>(channelName: string, router?: IClientRouter<TContext>): T;
|
||||
}
|
||||
|
||||
interface IReader {
|
||||
@@ -659,8 +662,11 @@ export class IPCServer<TContext = string> implements IChannelServer<TContext>, I
|
||||
private channels = new Map<string, IServerChannel<TContext>>();
|
||||
private _connections = new Set<Connection<TContext>>();
|
||||
|
||||
private readonly _onDidChangeConnections = new Emitter<Connection<TContext>>();
|
||||
readonly onDidChangeConnections: Event<Connection<TContext>> = this._onDidChangeConnections.event;
|
||||
private readonly _onDidAddConnection = new Emitter<Connection<TContext>>();
|
||||
readonly onDidAddConnection: Event<Connection<TContext>> = this._onDidAddConnection.event;
|
||||
|
||||
private readonly _onDidRemoveConnection = new Emitter<Connection<TContext>>();
|
||||
readonly onDidRemoveConnection: Event<Connection<TContext>> = this._onDidRemoveConnection.event;
|
||||
|
||||
get connections(): Connection<TContext>[] {
|
||||
const result: Connection<TContext>[] = [];
|
||||
@@ -683,30 +689,59 @@ export class IPCServer<TContext = string> implements IChannelServer<TContext>, I
|
||||
|
||||
const connection: Connection<TContext> = { channelServer, channelClient, ctx };
|
||||
this._connections.add(connection);
|
||||
this._onDidChangeConnections.fire(connection);
|
||||
this._onDidAddConnection.fire(connection);
|
||||
|
||||
onDidClientDisconnect(() => {
|
||||
channelServer.dispose();
|
||||
channelClient.dispose();
|
||||
this._connections.delete(connection);
|
||||
this._onDidRemoveConnection.fire(connection);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getChannel<T extends IChannel>(channelName: string, router: IClientRouter<TContext>): T {
|
||||
/**
|
||||
* Get a channel from a remote client. When passed a router,
|
||||
* one can specify which client it wants to call and listen to/from.
|
||||
* Otherwise, when calling without a router, a random client will
|
||||
* be selected and when listening without a router, every client
|
||||
* will be listened to.
|
||||
*/
|
||||
getChannel<T extends IChannel>(channelName: string, router: IClientRouter<TContext>): T;
|
||||
getChannel<T extends IChannel>(channelName: string, clientFilter: (client: Client<TContext>) => boolean): T;
|
||||
getChannel<T extends IChannel>(channelName: string, routerOrClientFilter: IClientRouter<TContext> | ((client: Client<TContext>) => boolean)): T {
|
||||
const that = this;
|
||||
|
||||
return {
|
||||
call(command: string, arg?: any, cancellationToken?: CancellationToken) {
|
||||
const channelPromise = router.routeCall(that, command, arg)
|
||||
call(command: string, arg?: any, cancellationToken?: CancellationToken): Promise<T> {
|
||||
let connectionPromise: Promise<Client<TContext>>;
|
||||
|
||||
if (isFunction(routerOrClientFilter)) {
|
||||
// when no router is provided, we go random client picking
|
||||
let connection = getRandomElement(that.connections.filter(routerOrClientFilter));
|
||||
|
||||
connectionPromise = connection
|
||||
// if we found a client, let's call on it
|
||||
? Promise.resolve(connection)
|
||||
// else, let's wait for a client to come along
|
||||
: Event.toPromise(Event.filter(that.onDidAddConnection, routerOrClientFilter));
|
||||
} else {
|
||||
connectionPromise = routerOrClientFilter.routeCall(that, command, arg);
|
||||
}
|
||||
|
||||
const channelPromise = connectionPromise
|
||||
.then(connection => (connection as Connection<TContext>).channelClient.getChannel(channelName));
|
||||
|
||||
return getDelayedChannel(channelPromise)
|
||||
.call(command, arg, cancellationToken);
|
||||
},
|
||||
listen(event: string, arg: any) {
|
||||
const channelPromise = router.routeEvent(that, event, arg)
|
||||
listen(event: string, arg: any): Event<T> {
|
||||
if (isFunction(routerOrClientFilter)) {
|
||||
return that.getMulticastEvent(channelName, routerOrClientFilter, event, arg);
|
||||
}
|
||||
|
||||
const channelPromise = routerOrClientFilter.routeEvent(that, event, arg)
|
||||
.then(connection => (connection as Connection<TContext>).channelClient.getChannel(channelName));
|
||||
|
||||
return getDelayedChannel(channelPromise)
|
||||
@@ -715,6 +750,58 @@ export class IPCServer<TContext = string> implements IChannelServer<TContext>, I
|
||||
} as T;
|
||||
}
|
||||
|
||||
private getMulticastEvent<T extends IChannel>(channelName: string, clientFilter: (client: Client<TContext>) => boolean, eventName: string, arg: any): Event<T> {
|
||||
const that = this;
|
||||
let disposables = new DisposableStore();
|
||||
|
||||
// Create an emitter which hooks up to all clients
|
||||
// as soon as first listener is added. It also
|
||||
// disconnects from all clients as soon as the last listener
|
||||
// is removed.
|
||||
const emitter = new Emitter<T>({
|
||||
onFirstListenerAdd: () => {
|
||||
disposables = new DisposableStore();
|
||||
|
||||
// The event multiplexer is useful since the active
|
||||
// client list is dynamic. We need to hook up and disconnection
|
||||
// to/from clients as they come and go.
|
||||
const eventMultiplexer = new EventMultiplexer<T>();
|
||||
const map = new Map<Connection<TContext>, IDisposable>();
|
||||
|
||||
const onDidAddConnection = (connection: Connection<TContext>) => {
|
||||
const channel = connection.channelClient.getChannel(channelName);
|
||||
const event = channel.listen<T>(eventName, arg);
|
||||
const disposable = eventMultiplexer.add(event);
|
||||
|
||||
map.set(connection, disposable);
|
||||
};
|
||||
|
||||
const onDidRemoveConnection = (connection: Connection<TContext>) => {
|
||||
const disposable = map.get(connection);
|
||||
|
||||
if (!disposable) {
|
||||
return;
|
||||
}
|
||||
|
||||
disposable.dispose();
|
||||
map.delete(connection);
|
||||
};
|
||||
|
||||
that.connections.filter(clientFilter).forEach(onDidAddConnection);
|
||||
Event.filter(that.onDidAddConnection, clientFilter)(onDidAddConnection, undefined, disposables);
|
||||
that.onDidRemoveConnection(onDidRemoveConnection, undefined, disposables);
|
||||
eventMultiplexer.event(emitter.fire, emitter, disposables);
|
||||
|
||||
disposables.add(eventMultiplexer);
|
||||
},
|
||||
onLastListenerRemove: () => {
|
||||
disposables.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
return emitter.event;
|
||||
}
|
||||
|
||||
registerChannel(channelName: string, channel: IServerChannel<TContext>): void {
|
||||
this.channels.set(channelName, channel);
|
||||
|
||||
@@ -726,7 +813,8 @@ export class IPCServer<TContext = string> implements IChannelServer<TContext>, I
|
||||
dispose(): void {
|
||||
this.channels.clear();
|
||||
this._connections.clear();
|
||||
this._onDidChangeConnections.dispose();
|
||||
this._onDidAddConnection.dispose();
|
||||
this._onDidRemoveConnection.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -827,7 +915,7 @@ export class StaticRouter<TContext = string> implements IClientRouter<TContext>
|
||||
}
|
||||
}
|
||||
|
||||
await Event.toPromise(hub.onDidChangeConnections);
|
||||
await Event.toPromise(hub.onDidAddConnection);
|
||||
return await this.route(hub);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,4 +415,80 @@ suite('Base IPC', function () {
|
||||
return assert.equal(r, 'Super Context');
|
||||
});
|
||||
});
|
||||
|
||||
suite('one to many', function () {
|
||||
test('all clients get pinged', async function () {
|
||||
const service = new TestService();
|
||||
const channel = new TestChannel(service);
|
||||
const server = new TestIPCServer();
|
||||
server.registerChannel('channel', channel);
|
||||
|
||||
let client1GotPinged = false;
|
||||
const client1 = server.createConnection('client1');
|
||||
const ipcService1 = new TestChannelClient(client1.getChannel('channel'));
|
||||
ipcService1.onPong(() => client1GotPinged = true);
|
||||
|
||||
let client2GotPinged = false;
|
||||
const client2 = server.createConnection('client2');
|
||||
const ipcService2 = new TestChannelClient(client2.getChannel('channel'));
|
||||
ipcService2.onPong(() => client2GotPinged = true);
|
||||
|
||||
await timeout(1);
|
||||
service.ping('hello');
|
||||
|
||||
await timeout(1);
|
||||
assert(client1GotPinged, 'client 1 got pinged');
|
||||
assert(client2GotPinged, 'client 2 got pinged');
|
||||
|
||||
client1.dispose();
|
||||
client2.dispose();
|
||||
server.dispose();
|
||||
});
|
||||
|
||||
test('server gets pings from all clients (broadcast channel)', async function () {
|
||||
const server = new TestIPCServer();
|
||||
|
||||
const client1 = server.createConnection('client1');
|
||||
const clientService1 = new TestService();
|
||||
const clientChannel1 = new TestChannel(clientService1);
|
||||
client1.registerChannel('channel', clientChannel1);
|
||||
|
||||
const pings: string[] = [];
|
||||
const channel = server.getChannel('channel', () => true);
|
||||
const service = new TestChannelClient(channel);
|
||||
service.onPong(msg => pings.push(msg));
|
||||
|
||||
await timeout(1);
|
||||
clientService1.ping('hello 1');
|
||||
|
||||
await timeout(1);
|
||||
assert.deepEqual(pings, ['hello 1']);
|
||||
|
||||
const client2 = server.createConnection('client2');
|
||||
const clientService2 = new TestService();
|
||||
const clientChannel2 = new TestChannel(clientService2);
|
||||
client2.registerChannel('channel', clientChannel2);
|
||||
|
||||
await timeout(1);
|
||||
clientService2.ping('hello 2');
|
||||
|
||||
await timeout(1);
|
||||
assert.deepEqual(pings, ['hello 1', 'hello 2']);
|
||||
|
||||
client1.dispose();
|
||||
clientService1.ping('hello 1');
|
||||
|
||||
await timeout(1);
|
||||
assert.deepEqual(pings, ['hello 1', 'hello 2']);
|
||||
|
||||
await timeout(1);
|
||||
clientService2.ping('hello again 2');
|
||||
|
||||
await timeout(1);
|
||||
assert.deepEqual(pings, ['hello 1', 'hello 2', 'hello again 2']);
|
||||
|
||||
client2.dispose();
|
||||
server.dispose();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="10px" height="16px" viewBox="0 0 10 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 40.3 (33839) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>arrow-left</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Octicons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="arrow-left" fill="#c5c5c5">
|
||||
<polygon id="Shape" points="6 3 0 8 6 13 6 10 10 10 10 6 6 6"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 594 B |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="10px" height="16px" viewBox="0 0 10 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 40.3 (33839) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>arrow-left</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Octicons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="arrow-left" fill="#424242">
|
||||
<polygon id="Shape" points="6 3 0 8 6 13 6 10 10 10 10 6 6 6"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 594 B |
245
src/vs/base/parts/quickinput/browser/media/quickInput.css
Normal file
245
src/vs/base/parts/quickinput/browser/media/quickInput.css
Normal file
@@ -0,0 +1,245 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.quick-input-widget {
|
||||
position: absolute;
|
||||
width: 600px;
|
||||
z-index: 2000;
|
||||
padding-bottom: 6px;
|
||||
left: 50%;
|
||||
margin-left: -300px;
|
||||
}
|
||||
|
||||
.quick-input-titlebar {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.quick-input-left-action-bar {
|
||||
display: flex;
|
||||
margin-left: 4px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.quick-input-left-action-bar.monaco-action-bar .actions-container {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.quick-input-title {
|
||||
padding: 3px 0px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quick-input-right-action-bar {
|
||||
display: flex;
|
||||
margin-right: 4px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.quick-input-titlebar .monaco-action-bar .action-label.codicon {
|
||||
margin: 0;
|
||||
width: 19px;
|
||||
height: 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.quick-input-description {
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
.quick-input-header {
|
||||
display: flex;
|
||||
padding: 6px 6px 0px 6px;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.quick-input-and-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quick-input-check-all {
|
||||
align-self: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.quick-input-filter {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quick-input-box {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.quick-input-widget.show-checkboxes .quick-input-box,
|
||||
.quick-input-widget.show-checkboxes .quick-input-message {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.quick-input-visible-count {
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
}
|
||||
|
||||
.quick-input-count {
|
||||
align-self: center;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
.quick-input-count .monaco-count-badge {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.quick-input-action {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.quick-input-action .monaco-text-button {
|
||||
font-size: 85%;
|
||||
padding: 7px 6px 5.5px 6px;
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
.quick-input-message {
|
||||
margin-top: -1px;
|
||||
padding: 5px 5px 2px 5px;
|
||||
}
|
||||
|
||||
.quick-input-progress.monaco-progress-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quick-input-progress.monaco-progress-container,
|
||||
.quick-input-progress.monaco-progress-container .progress-bit {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.quick-input-list {
|
||||
line-height: 22px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.quick-input-list .monaco-list {
|
||||
overflow: hidden;
|
||||
max-height: calc(20 * 22px);
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry {
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry.quick-input-list-separator-border {
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
}
|
||||
|
||||
.quick-input-list .monaco-list-row:first-child .quick-input-list-entry.quick-input-list-separator-border {
|
||||
border-top-style: none;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-label {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-checkbox {
|
||||
align-self: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-rows {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.quick-input-widget.show-checkboxes .quick-input-list .quick-input-list-rows {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.quick-input-widget .quick-input-list .quick-input-list-checkbox {
|
||||
display: none;
|
||||
}
|
||||
.quick-input-widget.show-checkboxes .quick-input-list .quick-input-list-checkbox {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-rows > .quick-input-list-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-rows > .quick-input-list-row .codicon {
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-rows .monaco-highlighted-label span {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-label-meta {
|
||||
opacity: 0.7;
|
||||
line-height: normal;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quick-input-list .monaco-highlighted-label .highlight {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-separator {
|
||||
margin-right: 18px;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry.has-actions:hover .quick-input-list-separator,
|
||||
.quick-input-list .monaco-list-row.focused .quick-input-list-entry.has-actions .quick-input-list-separator {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry-action-bar {
|
||||
display: none;
|
||||
flex: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry-action-bar .action-label.codicon {
|
||||
margin: 0;
|
||||
width: 19px;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry-action-bar {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry-action-bar ul:first-child .action-label.codicon {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry-action-bar ul:last-child .action-label.codicon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.quick-input-list .quick-input-list-entry:hover .quick-input-list-entry-action-bar,
|
||||
.quick-input-list .monaco-list-row.focused .quick-input-list-entry-action-bar {
|
||||
display: flex;
|
||||
}
|
||||
1556
src/vs/base/parts/quickinput/browser/quickInput.ts
Normal file
1556
src/vs/base/parts/quickinput/browser/quickInput.ts
Normal file
File diff suppressed because it is too large
Load Diff
116
src/vs/base/parts/quickinput/browser/quickInputBox.ts
Normal file
116
src/vs/base/parts/quickinput/browser/quickInputBox.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/quickInput';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { InputBox, IRange, MessageType, IInputBoxStyles } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
export class QuickInputBox extends Disposable {
|
||||
|
||||
private container: HTMLElement;
|
||||
private inputBox: InputBox;
|
||||
|
||||
constructor(
|
||||
private parent: HTMLElement
|
||||
) {
|
||||
super();
|
||||
this.container = dom.append(this.parent, $('.quick-input-box'));
|
||||
this.inputBox = this._register(new InputBox(this.container, undefined));
|
||||
}
|
||||
|
||||
onKeyDown = (handler: (event: StandardKeyboardEvent) => void): IDisposable => {
|
||||
return dom.addDisposableListener(this.inputBox.inputElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
|
||||
handler(new StandardKeyboardEvent(e));
|
||||
});
|
||||
};
|
||||
|
||||
onMouseDown = (handler: (event: StandardMouseEvent) => void): IDisposable => {
|
||||
return dom.addDisposableListener(this.inputBox.inputElement, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => {
|
||||
handler(new StandardMouseEvent(e));
|
||||
});
|
||||
};
|
||||
|
||||
onDidChange = (handler: (event: string) => void): IDisposable => {
|
||||
return this.inputBox.onDidChange(handler);
|
||||
};
|
||||
|
||||
get value() {
|
||||
return this.inputBox.value;
|
||||
}
|
||||
|
||||
set value(value: string) {
|
||||
this.inputBox.value = value;
|
||||
}
|
||||
|
||||
select(range: IRange | null = null): void {
|
||||
this.inputBox.select(range);
|
||||
}
|
||||
|
||||
setPlaceholder(placeholder: string) {
|
||||
this.inputBox.setPlaceHolder(placeholder);
|
||||
}
|
||||
|
||||
get placeholder() {
|
||||
return this.inputBox.inputElement.getAttribute('placeholder') || '';
|
||||
}
|
||||
|
||||
set placeholder(placeholder: string) {
|
||||
this.inputBox.setPlaceHolder(placeholder);
|
||||
}
|
||||
|
||||
get password() {
|
||||
return this.inputBox.inputElement.type === 'password';
|
||||
}
|
||||
|
||||
set password(password: boolean) {
|
||||
this.inputBox.inputElement.type = password ? 'password' : 'text';
|
||||
}
|
||||
|
||||
set enabled(enabled: boolean) {
|
||||
this.inputBox.setEnabled(enabled);
|
||||
}
|
||||
|
||||
hasFocus(): boolean {
|
||||
return this.inputBox.hasFocus();
|
||||
}
|
||||
|
||||
setAttribute(name: string, value: string) {
|
||||
this.inputBox.inputElement.setAttribute(name, value);
|
||||
}
|
||||
|
||||
removeAttribute(name: string) {
|
||||
this.inputBox.inputElement.removeAttribute(name);
|
||||
}
|
||||
|
||||
showDecoration(decoration: Severity): void {
|
||||
if (decoration === Severity.Ignore) {
|
||||
this.inputBox.hideMessage();
|
||||
} else {
|
||||
this.inputBox.showMessage({ type: decoration === Severity.Info ? MessageType.INFO : decoration === Severity.Warning ? MessageType.WARNING : MessageType.ERROR, content: '' });
|
||||
}
|
||||
}
|
||||
|
||||
stylesForType(decoration: Severity) {
|
||||
return this.inputBox.stylesForType(decoration === Severity.Info ? MessageType.INFO : decoration === Severity.Warning ? MessageType.WARNING : MessageType.ERROR);
|
||||
}
|
||||
|
||||
setFocus(): void {
|
||||
this.inputBox.focus();
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
this.inputBox.layout();
|
||||
}
|
||||
|
||||
style(styles: IInputBoxStyles) {
|
||||
this.inputBox.style(styles);
|
||||
}
|
||||
}
|
||||
593
src/vs/base/parts/quickinput/browser/quickInputList.ts
Normal file
593
src/vs/base/parts/quickinput/browser/quickInputList.ts
Normal file
@@ -0,0 +1,593 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/quickInput';
|
||||
import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator } from 'vs/base/parts/quickinput/common/quickInput';
|
||||
import { IMatch } from 'vs/base/common/filters';
|
||||
import { matchesFuzzyCodiconAware, parseCodicons } from 'vs/base/common/codicon';
|
||||
import { compareAnything } from 'vs/base/common/comparers';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { assign } from 'vs/base/common/objects';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel';
|
||||
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { range } from 'vs/base/common/arrays';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { getIconClass } from 'vs/base/parts/quickinput/browser/quickInputUtils';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
import { IQuickInputOptions } from 'vs/base/parts/quickinput/browser/quickInput';
|
||||
import { IListOptions, List, IListStyles } from 'vs/base/browser/ui/list/listWidget';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
interface IListElement {
|
||||
readonly index: number;
|
||||
readonly item: IQuickPickItem;
|
||||
readonly saneLabel: string;
|
||||
readonly saneDescription?: string;
|
||||
readonly saneDetail?: string;
|
||||
readonly checked: boolean;
|
||||
readonly separator?: IQuickPickSeparator;
|
||||
readonly fireButtonTriggered: (event: IQuickPickItemButtonEvent<IQuickPickItem>) => void;
|
||||
}
|
||||
|
||||
class ListElement implements IListElement {
|
||||
index!: number;
|
||||
item!: IQuickPickItem;
|
||||
saneLabel!: string;
|
||||
saneDescription?: string;
|
||||
saneDetail?: string;
|
||||
hidden = false;
|
||||
private readonly _onChecked = new Emitter<boolean>();
|
||||
onChecked = this._onChecked.event;
|
||||
_checked?: boolean;
|
||||
get checked() {
|
||||
return !!this._checked;
|
||||
}
|
||||
set checked(value: boolean) {
|
||||
if (value !== this._checked) {
|
||||
this._checked = value;
|
||||
this._onChecked.fire(value);
|
||||
}
|
||||
}
|
||||
separator?: IQuickPickSeparator;
|
||||
labelHighlights?: IMatch[];
|
||||
descriptionHighlights?: IMatch[];
|
||||
detailHighlights?: IMatch[];
|
||||
fireButtonTriggered!: (event: IQuickPickItemButtonEvent<IQuickPickItem>) => void;
|
||||
|
||||
constructor(init: IListElement) {
|
||||
assign(this, init);
|
||||
}
|
||||
}
|
||||
|
||||
interface IListElementTemplateData {
|
||||
entry: HTMLDivElement;
|
||||
checkbox: HTMLInputElement;
|
||||
label: IconLabel;
|
||||
detail: HighlightedLabel;
|
||||
separator: HTMLDivElement;
|
||||
actionBar: ActionBar;
|
||||
element: ListElement;
|
||||
toDisposeElement: IDisposable[];
|
||||
toDisposeTemplate: IDisposable[];
|
||||
}
|
||||
|
||||
class ListElementRenderer implements IListRenderer<ListElement, IListElementTemplateData> {
|
||||
|
||||
static readonly ID = 'listelement';
|
||||
|
||||
get templateId() {
|
||||
return ListElementRenderer.ID;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): IListElementTemplateData {
|
||||
const data: IListElementTemplateData = Object.create(null);
|
||||
data.toDisposeElement = [];
|
||||
data.toDisposeTemplate = [];
|
||||
|
||||
data.entry = dom.append(container, $('.quick-input-list-entry'));
|
||||
|
||||
// Checkbox
|
||||
const label = dom.append(data.entry, $('label.quick-input-list-label'));
|
||||
data.checkbox = <HTMLInputElement>dom.append(label, $('input.quick-input-list-checkbox'));
|
||||
data.checkbox.type = 'checkbox';
|
||||
data.toDisposeTemplate.push(dom.addStandardDisposableListener(data.checkbox, dom.EventType.CHANGE, e => {
|
||||
data.element.checked = data.checkbox.checked;
|
||||
}));
|
||||
|
||||
// Rows
|
||||
const rows = dom.append(label, $('.quick-input-list-rows'));
|
||||
const row1 = dom.append(rows, $('.quick-input-list-row'));
|
||||
const row2 = dom.append(rows, $('.quick-input-list-row'));
|
||||
|
||||
// Label
|
||||
data.label = new IconLabel(row1, { supportHighlights: true, supportDescriptionHighlights: true, supportCodicons: true });
|
||||
|
||||
// Detail
|
||||
const detailContainer = dom.append(row2, $('.quick-input-list-label-meta'));
|
||||
data.detail = new HighlightedLabel(detailContainer, true);
|
||||
|
||||
// Separator
|
||||
data.separator = dom.append(data.entry, $('.quick-input-list-separator'));
|
||||
|
||||
// Actions
|
||||
data.actionBar = new ActionBar(data.entry);
|
||||
data.actionBar.domNode.classList.add('quick-input-list-entry-action-bar');
|
||||
data.toDisposeTemplate.push(data.actionBar);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
renderElement(element: ListElement, index: number, data: IListElementTemplateData): void {
|
||||
data.toDisposeElement = dispose(data.toDisposeElement);
|
||||
data.element = element;
|
||||
data.checkbox.checked = element.checked;
|
||||
data.toDisposeElement.push(element.onChecked(checked => data.checkbox.checked = checked));
|
||||
|
||||
const { labelHighlights, descriptionHighlights, detailHighlights } = element;
|
||||
|
||||
// Label
|
||||
const options: IIconLabelValueOptions = Object.create(null);
|
||||
options.matches = labelHighlights || [];
|
||||
options.descriptionTitle = element.saneDescription;
|
||||
options.descriptionMatches = descriptionHighlights || [];
|
||||
options.extraClasses = element.item.iconClasses;
|
||||
data.label.setLabel(element.saneLabel, element.saneDescription, options);
|
||||
|
||||
// Meta
|
||||
data.detail.set(element.saneDetail, detailHighlights);
|
||||
|
||||
// ARIA label
|
||||
data.entry.setAttribute('aria-label', [element.saneLabel, element.saneDescription, element.saneDetail]
|
||||
.map(s => s && parseCodicons(s).text)
|
||||
.filter(s => !!s)
|
||||
.join(', '));
|
||||
|
||||
// Separator
|
||||
if (element.separator && element.separator.label) {
|
||||
data.separator.textContent = element.separator.label;
|
||||
data.separator.style.display = '';
|
||||
} else {
|
||||
data.separator.style.display = 'none';
|
||||
}
|
||||
if (element.separator) {
|
||||
dom.addClass(data.entry, 'quick-input-list-separator-border');
|
||||
} else {
|
||||
dom.removeClass(data.entry, 'quick-input-list-separator-border');
|
||||
}
|
||||
|
||||
// Actions
|
||||
data.actionBar.clear();
|
||||
const buttons = element.item.buttons;
|
||||
if (buttons && buttons.length) {
|
||||
data.actionBar.push(buttons.map((button, index) => {
|
||||
const action = new Action(`id-${index}`, '', button.iconClass || (button.iconPath ? getIconClass(button.iconPath) : undefined), true, () => {
|
||||
element.fireButtonTriggered({
|
||||
button,
|
||||
item: element.item
|
||||
});
|
||||
return Promise.resolve();
|
||||
});
|
||||
action.tooltip = button.tooltip || '';
|
||||
return action;
|
||||
}), { icon: true, label: false });
|
||||
dom.addClass(data.entry, 'has-actions');
|
||||
} else {
|
||||
dom.removeClass(data.entry, 'has-actions');
|
||||
}
|
||||
}
|
||||
|
||||
disposeElement(element: ListElement, index: number, data: IListElementTemplateData): void {
|
||||
data.toDisposeElement = dispose(data.toDisposeElement);
|
||||
}
|
||||
|
||||
disposeTemplate(data: IListElementTemplateData): void {
|
||||
data.toDisposeElement = dispose(data.toDisposeElement);
|
||||
data.toDisposeTemplate = dispose(data.toDisposeTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
class ListElementDelegate implements IListVirtualDelegate<ListElement> {
|
||||
|
||||
getHeight(element: ListElement): number {
|
||||
return element.saneDetail ? 44 : 22;
|
||||
}
|
||||
|
||||
getTemplateId(element: ListElement): string {
|
||||
return ListElementRenderer.ID;
|
||||
}
|
||||
}
|
||||
|
||||
export class QuickInputList {
|
||||
|
||||
readonly id: string;
|
||||
private container: HTMLElement;
|
||||
private list: List<ListElement>;
|
||||
private inputElements: Array<IQuickPickItem | IQuickPickSeparator> = [];
|
||||
private elements: ListElement[] = [];
|
||||
private elementsToIndexes = new Map<IQuickPickItem, number>();
|
||||
matchOnDescription = false;
|
||||
matchOnDetail = false;
|
||||
matchOnLabel = true;
|
||||
sortByLabel = true;
|
||||
private readonly _onChangedAllVisibleChecked = new Emitter<boolean>();
|
||||
onChangedAllVisibleChecked: Event<boolean> = this._onChangedAllVisibleChecked.event;
|
||||
private readonly _onChangedCheckedCount = new Emitter<number>();
|
||||
onChangedCheckedCount: Event<number> = this._onChangedCheckedCount.event;
|
||||
private readonly _onChangedVisibleCount = new Emitter<number>();
|
||||
onChangedVisibleCount: Event<number> = this._onChangedVisibleCount.event;
|
||||
private readonly _onChangedCheckedElements = new Emitter<IQuickPickItem[]>();
|
||||
onChangedCheckedElements: Event<IQuickPickItem[]> = this._onChangedCheckedElements.event;
|
||||
private readonly _onButtonTriggered = new Emitter<IQuickPickItemButtonEvent<IQuickPickItem>>();
|
||||
onButtonTriggered = this._onButtonTriggered.event;
|
||||
private readonly _onLeave = new Emitter<void>();
|
||||
onLeave: Event<void> = this._onLeave.event;
|
||||
private _fireCheckedEvents = true;
|
||||
private elementDisposables: IDisposable[] = [];
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
private parent: HTMLElement,
|
||||
id: string,
|
||||
options: IQuickInputOptions,
|
||||
) {
|
||||
this.id = id;
|
||||
this.container = dom.append(this.parent, $('.quick-input-list'));
|
||||
const delegate = new ListElementDelegate();
|
||||
this.list = options.createList('QuickInput', this.container, delegate, [new ListElementRenderer()], {
|
||||
identityProvider: { getId: element => element.saneLabel },
|
||||
openController: { shouldOpen: () => false }, // Workaround #58124
|
||||
setRowLineHeight: false,
|
||||
multipleSelectionSupport: false,
|
||||
horizontalScrolling: false,
|
||||
} as IListOptions<ListElement>);
|
||||
this.list.getHTMLElement().id = id;
|
||||
this.disposables.push(this.list);
|
||||
this.disposables.push(this.list.onKeyDown(e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
switch (event.keyCode) {
|
||||
case KeyCode.Space:
|
||||
this.toggleCheckbox();
|
||||
break;
|
||||
case KeyCode.KEY_A:
|
||||
if (platform.isMacintosh ? e.metaKey : e.ctrlKey) {
|
||||
this.list.setFocus(range(this.list.length));
|
||||
}
|
||||
break;
|
||||
case KeyCode.UpArrow:
|
||||
case KeyCode.PageUp:
|
||||
const focus1 = this.list.getFocus();
|
||||
if (focus1.length === 1 && focus1[0] === 0) {
|
||||
this._onLeave.fire();
|
||||
}
|
||||
break;
|
||||
case KeyCode.DownArrow:
|
||||
case KeyCode.PageDown:
|
||||
const focus2 = this.list.getFocus();
|
||||
if (focus2.length === 1 && focus2[0] === this.list.length - 1) {
|
||||
this._onLeave.fire();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}));
|
||||
this.disposables.push(this.list.onMouseDown(e => {
|
||||
if (e.browserEvent.button !== 2) {
|
||||
// Works around / fixes #64350.
|
||||
e.browserEvent.preventDefault();
|
||||
}
|
||||
}));
|
||||
this.disposables.push(dom.addDisposableListener(this.container, dom.EventType.CLICK, e => {
|
||||
if (e.x || e.y) { // Avoid 'click' triggered by 'space' on checkbox.
|
||||
this._onLeave.fire();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@memoize
|
||||
get onDidChangeFocus() {
|
||||
return Event.map(this.list.onFocusChange, e => e.elements.map(e => e.item));
|
||||
}
|
||||
|
||||
@memoize
|
||||
get onDidChangeSelection() {
|
||||
return Event.map(this.list.onSelectionChange, e => e.elements.map(e => e.item));
|
||||
}
|
||||
|
||||
getAllVisibleChecked() {
|
||||
return this.allVisibleChecked(this.elements, false);
|
||||
}
|
||||
|
||||
private allVisibleChecked(elements: ListElement[], whenNoneVisible = true) {
|
||||
for (let i = 0, n = elements.length; i < n; i++) {
|
||||
const element = elements[i];
|
||||
if (!element.hidden) {
|
||||
if (!element.checked) {
|
||||
return false;
|
||||
} else {
|
||||
whenNoneVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return whenNoneVisible;
|
||||
}
|
||||
|
||||
getCheckedCount() {
|
||||
let count = 0;
|
||||
const elements = this.elements;
|
||||
for (let i = 0, n = elements.length; i < n; i++) {
|
||||
if (elements[i].checked) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
getVisibleCount() {
|
||||
let count = 0;
|
||||
const elements = this.elements;
|
||||
for (let i = 0, n = elements.length; i < n; i++) {
|
||||
if (!elements[i].hidden) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
setAllVisibleChecked(checked: boolean) {
|
||||
try {
|
||||
this._fireCheckedEvents = false;
|
||||
this.elements.forEach(element => {
|
||||
if (!element.hidden) {
|
||||
element.checked = checked;
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
this._fireCheckedEvents = true;
|
||||
this.fireCheckedEvents();
|
||||
}
|
||||
}
|
||||
|
||||
setElements(inputElements: Array<IQuickPickItem | IQuickPickSeparator>): void {
|
||||
this.elementDisposables = dispose(this.elementDisposables);
|
||||
const fireButtonTriggered = (event: IQuickPickItemButtonEvent<IQuickPickItem>) => this.fireButtonTriggered(event);
|
||||
this.inputElements = inputElements;
|
||||
this.elements = inputElements.reduce((result, item, index) => {
|
||||
if (item.type !== 'separator') {
|
||||
const previous = index && inputElements[index - 1];
|
||||
result.push(new ListElement({
|
||||
index,
|
||||
item,
|
||||
saneLabel: item.label && item.label.replace(/\r?\n/g, ' '),
|
||||
saneDescription: item.description && item.description.replace(/\r?\n/g, ' '),
|
||||
saneDetail: item.detail && item.detail.replace(/\r?\n/g, ' '),
|
||||
checked: false,
|
||||
separator: previous && previous.type === 'separator' ? previous : undefined,
|
||||
fireButtonTriggered
|
||||
}));
|
||||
}
|
||||
return result;
|
||||
}, [] as ListElement[]);
|
||||
this.elementDisposables.push(...this.elements.map(element => element.onChecked(() => this.fireCheckedEvents())));
|
||||
|
||||
this.elementsToIndexes = this.elements.reduce((map, element, index) => {
|
||||
map.set(element.item, index);
|
||||
return map;
|
||||
}, new Map<IQuickPickItem, number>());
|
||||
this.list.splice(0, this.list.length); // Clear focus and selection first, sending the events when the list is empty.
|
||||
this.list.splice(0, this.list.length, this.elements);
|
||||
this._onChangedVisibleCount.fire(this.elements.length);
|
||||
}
|
||||
|
||||
getFocusedElements() {
|
||||
return this.list.getFocusedElements()
|
||||
.map(e => e.item);
|
||||
}
|
||||
|
||||
setFocusedElements(items: IQuickPickItem[]) {
|
||||
this.list.setFocus(items
|
||||
.filter(item => this.elementsToIndexes.has(item))
|
||||
.map(item => this.elementsToIndexes.get(item)!));
|
||||
if (items.length > 0) {
|
||||
this.list.reveal(this.list.getFocus()[0]);
|
||||
}
|
||||
}
|
||||
|
||||
getActiveDescendant() {
|
||||
return this.list.getHTMLElement().getAttribute('aria-activedescendant');
|
||||
}
|
||||
|
||||
getSelectedElements() {
|
||||
return this.list.getSelectedElements()
|
||||
.map(e => e.item);
|
||||
}
|
||||
|
||||
setSelectedElements(items: IQuickPickItem[]) {
|
||||
this.list.setSelection(items
|
||||
.filter(item => this.elementsToIndexes.has(item))
|
||||
.map(item => this.elementsToIndexes.get(item)!));
|
||||
}
|
||||
|
||||
getCheckedElements() {
|
||||
return this.elements.filter(e => e.checked)
|
||||
.map(e => e.item);
|
||||
}
|
||||
|
||||
setCheckedElements(items: IQuickPickItem[]) {
|
||||
try {
|
||||
this._fireCheckedEvents = false;
|
||||
const checked = new Set();
|
||||
for (const item of items) {
|
||||
checked.add(item);
|
||||
}
|
||||
for (const element of this.elements) {
|
||||
element.checked = checked.has(element.item);
|
||||
}
|
||||
} finally {
|
||||
this._fireCheckedEvents = true;
|
||||
this.fireCheckedEvents();
|
||||
}
|
||||
}
|
||||
|
||||
set enabled(value: boolean) {
|
||||
this.list.getHTMLElement().style.pointerEvents = value ? null : 'none';
|
||||
}
|
||||
|
||||
focus(what: 'First' | 'Last' | 'Next' | 'Previous' | 'NextPage' | 'PreviousPage'): void {
|
||||
if (!this.list.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((what === 'Next' || what === 'NextPage') && this.list.getFocus()[0] === this.list.length - 1) {
|
||||
what = 'First';
|
||||
}
|
||||
if ((what === 'Previous' || what === 'PreviousPage') && this.list.getFocus()[0] === 0) {
|
||||
what = 'Last';
|
||||
}
|
||||
|
||||
(this.list as any)['focus' + what]();
|
||||
this.list.reveal(this.list.getFocus()[0]);
|
||||
}
|
||||
|
||||
clearFocus() {
|
||||
this.list.setFocus([]);
|
||||
}
|
||||
|
||||
domFocus() {
|
||||
this.list.domFocus();
|
||||
}
|
||||
|
||||
layout(maxHeight?: number): void {
|
||||
this.list.getHTMLElement().style.maxHeight = maxHeight ? `calc(${Math.floor(maxHeight / 44) * 44}px)` : '';
|
||||
this.list.layout();
|
||||
}
|
||||
|
||||
filter(query: string) {
|
||||
if (!(this.sortByLabel || this.matchOnLabel || this.matchOnDescription || this.matchOnDetail)) {
|
||||
return;
|
||||
}
|
||||
query = query.trim();
|
||||
|
||||
// Reset filtering
|
||||
if (!query || !(this.matchOnLabel || this.matchOnDescription || this.matchOnDetail)) {
|
||||
this.elements.forEach(element => {
|
||||
element.labelHighlights = undefined;
|
||||
element.descriptionHighlights = undefined;
|
||||
element.detailHighlights = undefined;
|
||||
element.hidden = false;
|
||||
const previous = element.index && this.inputElements[element.index - 1];
|
||||
element.separator = previous && previous.type === 'separator' ? previous : undefined;
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by value (since we support codicons, use codicon aware fuzzy matching)
|
||||
else {
|
||||
this.elements.forEach(element => {
|
||||
const labelHighlights = this.matchOnLabel ? withNullAsUndefined(matchesFuzzyCodiconAware(query, parseCodicons(element.saneLabel))) : undefined;
|
||||
const descriptionHighlights = this.matchOnDescription ? withNullAsUndefined(matchesFuzzyCodiconAware(query, parseCodicons(element.saneDescription || ''))) : undefined;
|
||||
const detailHighlights = this.matchOnDetail ? withNullAsUndefined(matchesFuzzyCodiconAware(query, parseCodicons(element.saneDetail || ''))) : undefined;
|
||||
|
||||
if (labelHighlights || descriptionHighlights || detailHighlights) {
|
||||
element.labelHighlights = labelHighlights;
|
||||
element.descriptionHighlights = descriptionHighlights;
|
||||
element.detailHighlights = detailHighlights;
|
||||
element.hidden = false;
|
||||
} else {
|
||||
element.labelHighlights = undefined;
|
||||
element.descriptionHighlights = undefined;
|
||||
element.detailHighlights = undefined;
|
||||
element.hidden = !element.item.alwaysShow;
|
||||
}
|
||||
element.separator = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
const shownElements = this.elements.filter(element => !element.hidden);
|
||||
|
||||
// Sort by value
|
||||
if (this.sortByLabel && query) {
|
||||
const normalizedSearchValue = query.toLowerCase();
|
||||
shownElements.sort((a, b) => {
|
||||
return compareEntries(a, b, normalizedSearchValue);
|
||||
});
|
||||
}
|
||||
|
||||
this.elementsToIndexes = shownElements.reduce((map, element, index) => {
|
||||
map.set(element.item, index);
|
||||
return map;
|
||||
}, new Map<IQuickPickItem, number>());
|
||||
this.list.splice(0, this.list.length, shownElements);
|
||||
this.list.setFocus([]);
|
||||
this.list.layout();
|
||||
|
||||
this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked());
|
||||
this._onChangedVisibleCount.fire(shownElements.length);
|
||||
}
|
||||
|
||||
toggleCheckbox() {
|
||||
try {
|
||||
this._fireCheckedEvents = false;
|
||||
const elements = this.list.getFocusedElements();
|
||||
const allChecked = this.allVisibleChecked(elements);
|
||||
for (const element of elements) {
|
||||
element.checked = !allChecked;
|
||||
}
|
||||
} finally {
|
||||
this._fireCheckedEvents = true;
|
||||
this.fireCheckedEvents();
|
||||
}
|
||||
}
|
||||
|
||||
display(display: boolean) {
|
||||
this.container.style.display = display ? '' : 'none';
|
||||
}
|
||||
|
||||
isDisplayed() {
|
||||
return this.container.style.display !== 'none';
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.elementDisposables = dispose(this.elementDisposables);
|
||||
this.disposables = dispose(this.disposables);
|
||||
}
|
||||
|
||||
private fireCheckedEvents() {
|
||||
if (this._fireCheckedEvents) {
|
||||
this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked());
|
||||
this._onChangedCheckedCount.fire(this.getCheckedCount());
|
||||
this._onChangedCheckedElements.fire(this.getCheckedElements());
|
||||
}
|
||||
}
|
||||
|
||||
private fireButtonTriggered(event: IQuickPickItemButtonEvent<IQuickPickItem>) {
|
||||
this._onButtonTriggered.fire(event);
|
||||
}
|
||||
|
||||
style(styles: IListStyles) {
|
||||
this.list.style(styles);
|
||||
}
|
||||
}
|
||||
|
||||
function compareEntries(elementA: ListElement, elementB: ListElement, lookFor: string): number {
|
||||
|
||||
const labelHighlightsA = elementA.labelHighlights || [];
|
||||
const labelHighlightsB = elementB.labelHighlights || [];
|
||||
if (labelHighlightsA.length && !labelHighlightsB.length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!labelHighlightsA.length && labelHighlightsB.length) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return compareAnything(elementA.saneLabel, elementB.saneLabel, lookFor);
|
||||
}
|
||||
31
src/vs/base/parts/quickinput/browser/quickInputUtils.ts
Normal file
31
src/vs/base/parts/quickinput/browser/quickInputUtils.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/quickInput';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IdGenerator } from 'vs/base/common/idGenerator';
|
||||
|
||||
const iconPathToClass: Record<string, string> = {};
|
||||
const iconClassGenerator = new IdGenerator('quick-input-button-icon-');
|
||||
|
||||
export function getIconClass(iconPath: { dark: URI; light?: URI; } | undefined): string | undefined {
|
||||
if (!iconPath) {
|
||||
return undefined;
|
||||
}
|
||||
let iconClass: string;
|
||||
|
||||
const key = iconPath.dark.toString();
|
||||
if (iconPathToClass[key]) {
|
||||
iconClass = iconPathToClass[key];
|
||||
} else {
|
||||
iconClass = iconClassGenerator.nextId();
|
||||
dom.createCSSRule(`.${iconClass}`, `background-image: ${dom.asCSSUrl(iconPath.light || iconPath.dark)}`);
|
||||
dom.createCSSRule(`.vs-dark .${iconClass}, .hc-black .${iconClass}`, `background-image: ${dom.asCSSUrl(iconPath.dark)}`);
|
||||
iconPathToClass[key] = iconClass;
|
||||
}
|
||||
|
||||
return iconClass;
|
||||
}
|
||||
256
src/vs/base/parts/quickinput/common/quickInput.ts
Normal file
256
src/vs/base/parts/quickinput/common/quickInput.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
|
||||
export interface IQuickPickItem {
|
||||
type?: 'item';
|
||||
id?: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
detail?: string;
|
||||
iconClasses?: string[];
|
||||
buttons?: IQuickInputButton[];
|
||||
picked?: boolean;
|
||||
alwaysShow?: boolean;
|
||||
}
|
||||
|
||||
export interface IQuickPickSeparator {
|
||||
type: 'separator';
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface IKeyMods {
|
||||
readonly ctrlCmd: boolean;
|
||||
readonly alt: boolean;
|
||||
}
|
||||
|
||||
export interface IQuickNavigateConfiguration {
|
||||
keybindings: ResolvedKeybinding[];
|
||||
}
|
||||
|
||||
export interface IPickOptions<T extends IQuickPickItem> {
|
||||
|
||||
/**
|
||||
* an optional string to show as placeholder in the input box to guide the user what she picks on
|
||||
*/
|
||||
placeHolder?: string;
|
||||
|
||||
/**
|
||||
* an optional flag to include the description when filtering the picks
|
||||
*/
|
||||
matchOnDescription?: boolean;
|
||||
|
||||
/**
|
||||
* an optional flag to include the detail when filtering the picks
|
||||
*/
|
||||
matchOnDetail?: boolean;
|
||||
|
||||
/**
|
||||
* an optional flag to filter the picks based on label. Defaults to true.
|
||||
*/
|
||||
matchOnLabel?: boolean;
|
||||
|
||||
/**
|
||||
* an option flag to control whether focus is always automatically brought to a list item. Defaults to true.
|
||||
*/
|
||||
autoFocusOnList?: boolean;
|
||||
|
||||
/**
|
||||
* an optional flag to not close the picker on focus lost
|
||||
*/
|
||||
ignoreFocusLost?: boolean;
|
||||
|
||||
/**
|
||||
* an optional flag to make this picker multi-select
|
||||
*/
|
||||
canPickMany?: boolean;
|
||||
|
||||
/**
|
||||
* enables quick navigate in the picker to open an element without typing
|
||||
*/
|
||||
quickNavigate?: IQuickNavigateConfiguration;
|
||||
|
||||
/**
|
||||
* a context key to set when this picker is active
|
||||
*/
|
||||
contextKey?: string;
|
||||
|
||||
/**
|
||||
* an optional property for the item to focus initially.
|
||||
*/
|
||||
activeItem?: Promise<T> | T;
|
||||
|
||||
onKeyMods?: (keyMods: IKeyMods) => void;
|
||||
onDidFocus?: (entry: T) => void;
|
||||
onDidTriggerItemButton?: (context: IQuickPickItemButtonContext<T>) => void;
|
||||
}
|
||||
|
||||
export interface IInputOptions {
|
||||
|
||||
/**
|
||||
* the value to prefill in the input box
|
||||
*/
|
||||
value?: string;
|
||||
|
||||
/**
|
||||
* the selection of value, default to the whole word
|
||||
*/
|
||||
valueSelection?: [number, number];
|
||||
|
||||
/**
|
||||
* the text to display underneath the input box
|
||||
*/
|
||||
prompt?: string;
|
||||
|
||||
/**
|
||||
* an optional string to show as placeholder in the input box to guide the user what to type
|
||||
*/
|
||||
placeHolder?: string;
|
||||
|
||||
/**
|
||||
* set to true to show a password prompt that will not show the typed value
|
||||
*/
|
||||
password?: boolean;
|
||||
|
||||
ignoreFocusLost?: boolean;
|
||||
|
||||
/**
|
||||
* an optional function that is used to validate user input.
|
||||
*/
|
||||
validateInput?: (input: string) => Promise<string | null | undefined>;
|
||||
}
|
||||
|
||||
export interface IQuickInput {
|
||||
|
||||
title: string | undefined;
|
||||
|
||||
description: string | undefined;
|
||||
|
||||
step: number | undefined;
|
||||
|
||||
totalSteps: number | undefined;
|
||||
|
||||
enabled: boolean;
|
||||
|
||||
contextKey: string | undefined;
|
||||
|
||||
busy: boolean;
|
||||
|
||||
ignoreFocusOut: boolean;
|
||||
|
||||
show(): void;
|
||||
|
||||
hide(): void;
|
||||
|
||||
onDidHide: Event<void>;
|
||||
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface IQuickPick<T extends IQuickPickItem> extends IQuickInput {
|
||||
|
||||
value: string;
|
||||
|
||||
placeholder: string | undefined;
|
||||
|
||||
readonly onDidChangeValue: Event<string>;
|
||||
|
||||
readonly onDidAccept: Event<void>;
|
||||
|
||||
ok: boolean;
|
||||
|
||||
readonly onDidCustom: Event<void>;
|
||||
|
||||
customButton: boolean;
|
||||
|
||||
customLabel: string | undefined;
|
||||
|
||||
customHover: string | undefined;
|
||||
|
||||
buttons: ReadonlyArray<IQuickInputButton>;
|
||||
|
||||
readonly onDidTriggerButton: Event<IQuickInputButton>;
|
||||
|
||||
readonly onDidTriggerItemButton: Event<IQuickPickItemButtonEvent<T>>;
|
||||
|
||||
items: ReadonlyArray<T | IQuickPickSeparator>;
|
||||
|
||||
canSelectMany: boolean;
|
||||
|
||||
matchOnDescription: boolean;
|
||||
|
||||
matchOnDetail: boolean;
|
||||
|
||||
matchOnLabel: boolean;
|
||||
|
||||
sortByLabel: boolean;
|
||||
|
||||
autoFocusOnList: boolean;
|
||||
|
||||
quickNavigate: IQuickNavigateConfiguration | undefined;
|
||||
|
||||
activeItems: ReadonlyArray<T>;
|
||||
|
||||
readonly onDidChangeActive: Event<T[]>;
|
||||
|
||||
selectedItems: ReadonlyArray<T>;
|
||||
|
||||
readonly onDidChangeSelection: Event<T[]>;
|
||||
|
||||
readonly keyMods: IKeyMods;
|
||||
|
||||
valueSelection: Readonly<[number, number]> | undefined;
|
||||
|
||||
validationMessage: string | undefined;
|
||||
|
||||
inputHasFocus(): boolean;
|
||||
|
||||
focusOnInput(): void;
|
||||
}
|
||||
|
||||
export interface IInputBox extends IQuickInput {
|
||||
|
||||
value: string;
|
||||
|
||||
valueSelection: Readonly<[number, number]> | undefined;
|
||||
|
||||
placeholder: string | undefined;
|
||||
|
||||
password: boolean;
|
||||
|
||||
readonly onDidChangeValue: Event<string>;
|
||||
|
||||
readonly onDidAccept: Event<void>;
|
||||
|
||||
buttons: ReadonlyArray<IQuickInputButton>;
|
||||
|
||||
readonly onDidTriggerButton: Event<IQuickInputButton>;
|
||||
|
||||
prompt: string | undefined;
|
||||
|
||||
validationMessage: string | undefined;
|
||||
}
|
||||
|
||||
export interface IQuickInputButton {
|
||||
/** iconPath or iconClass required */
|
||||
iconPath?: { dark: URI; light?: URI; };
|
||||
/** iconPath or iconClass required */
|
||||
iconClass?: string;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
export interface IQuickPickItemButtonEvent<T extends IQuickPickItem> {
|
||||
button: IQuickInputButton;
|
||||
item: T;
|
||||
}
|
||||
|
||||
export interface IQuickPickItemButtonContext<T extends IQuickPickItem> extends IQuickPickItemButtonEvent<T> {
|
||||
removeItem(): void;
|
||||
}
|
||||
|
||||
export type QuickPickInput<T = IQuickPickItem> = T | IQuickPickSeparator;
|
||||
@@ -754,6 +754,7 @@ export class QuickOpenWidget extends Disposable implements IModelProvider, IThem
|
||||
else if (autoFocus.autoFocusLastEntry) {
|
||||
if (entries.length > 1) {
|
||||
this.tree.focusLast();
|
||||
this.tree.reveal(this.tree.getFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,10 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.monaco-quick-open-widget .quick-open-tree .quick-open-entry .monaco-highlighted-label .codicon {
|
||||
vertical-align: sub; /* vertically align codicon */
|
||||
}
|
||||
|
||||
.monaco-quick-open-widget .quick-open-tree .quick-open-entry-meta {
|
||||
opacity: 0.7;
|
||||
line-height: normal;
|
||||
|
||||
@@ -356,13 +356,15 @@ suite('IndexTreeModel', function () {
|
||||
assert.deepEqual(list[1].collapsible, false);
|
||||
assert.deepEqual(list[1].collapsed, false);
|
||||
|
||||
model.setCollapsed([0], true);
|
||||
assert.deepEqual(list.length, 1);
|
||||
assert.deepEqual(model.setCollapsed([0], true), false);
|
||||
assert.deepEqual(list[0].element, 0);
|
||||
assert.deepEqual(list[0].collapsible, false);
|
||||
assert.deepEqual(list[0].collapsed, true);
|
||||
assert.deepEqual(list[0].collapsed, false);
|
||||
assert.deepEqual(list[1].element, 10);
|
||||
assert.deepEqual(list[1].collapsible, false);
|
||||
assert.deepEqual(list[1].collapsed, false);
|
||||
|
||||
model.setCollapsed([0], false);
|
||||
assert.deepEqual(model.setCollapsed([0], false), false);
|
||||
assert.deepEqual(list[0].element, 0);
|
||||
assert.deepEqual(list[0].collapsible, false);
|
||||
assert.deepEqual(list[0].collapsed, false);
|
||||
@@ -379,13 +381,13 @@ suite('IndexTreeModel', function () {
|
||||
assert.deepEqual(list[1].collapsible, false);
|
||||
assert.deepEqual(list[1].collapsed, false);
|
||||
|
||||
model.setCollapsed([0], true);
|
||||
assert.deepEqual(model.setCollapsed([0], true), true);
|
||||
assert.deepEqual(list.length, 1);
|
||||
assert.deepEqual(list[0].element, 0);
|
||||
assert.deepEqual(list[0].collapsible, true);
|
||||
assert.deepEqual(list[0].collapsed, true);
|
||||
|
||||
model.setCollapsed([0], false);
|
||||
assert.deepEqual(model.setCollapsed([0], false), true);
|
||||
assert.deepEqual(list[0].element, 0);
|
||||
assert.deepEqual(list[0].collapsible, true);
|
||||
assert.deepEqual(list[0].collapsed, false);
|
||||
|
||||
@@ -404,7 +404,7 @@ suite('URI', () => {
|
||||
path = 'foo/bar';
|
||||
assert.equal(URI.file(path).path, '/foo/bar');
|
||||
path = './foo/bar';
|
||||
assert.equal(URI.file(path).path, '/./foo/bar'); // todo@joh missing normalization
|
||||
assert.equal(URI.file(path).path, '/./foo/bar'); // missing normalization
|
||||
|
||||
const fileUri1 = URI.parse(`file:foo/bar`);
|
||||
assert.equal(fileUri1.path, '/foo/bar');
|
||||
|
||||
@@ -26,35 +26,32 @@ export class DeferredPromise<T> {
|
||||
|
||||
public complete(value: T) {
|
||||
return new Promise(resolve => {
|
||||
process.nextTick(() => {
|
||||
this.completeCallback(value);
|
||||
resolve();
|
||||
});
|
||||
this.completeCallback(value);
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
public error(err: any) {
|
||||
return new Promise(resolve => {
|
||||
process.nextTick(() => {
|
||||
this.errorCallback(err);
|
||||
resolve();
|
||||
});
|
||||
this.errorCallback(err);
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
public cancel() {
|
||||
process.nextTick(() => {
|
||||
new Promise(resolve => {
|
||||
this.errorCallback(canceled());
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function toResource(this: any, path: string) {
|
||||
if (isWindows) {
|
||||
return URI.file(join('C:\\', Buffer.from(this.test.fullTitle()).toString('base64'), path));
|
||||
return URI.file(join('C:\\', btoa(this.test.fullTitle()), path));
|
||||
}
|
||||
|
||||
return URI.file(join('/', Buffer.from(this.test.fullTitle()).toString('base64'), path));
|
||||
return URI.file(join('/', btoa(this.test.fullTitle()), path));
|
||||
}
|
||||
|
||||
export function suiteRepeat(n: number, description: string, callback: (this: any) => void): void {
|
||||
|
||||
@@ -176,8 +176,8 @@ suite('Paths (Node Implementation)', () => {
|
||||
});
|
||||
|
||||
test('dirname', () => {
|
||||
assert.strictEqual(path.dirname(path.normalize(__filename)).substr(-11),
|
||||
isWindows ? 'test\\common' : 'test/common');
|
||||
assert.strictEqual(path.dirname(path.normalize(__filename)).substr(-9),
|
||||
isWindows ? 'test\\node' : 'test/node');
|
||||
|
||||
assert.strictEqual(path.posix.dirname('/a/b/'), '/a');
|
||||
assert.strictEqual(path.posix.dirname('/a/b'), '/a');
|
||||
@@ -7,47 +7,14 @@ import * as assert from 'assert';
|
||||
import * as os from 'os';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as fs from 'fs';
|
||||
import { Readable } from 'stream';
|
||||
import * as uuid from 'vs/base/common/uuid';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
import { isWindows, isLinux } from 'vs/base/common/platform';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { canNormalize } from 'vs/base/common/normalization';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
const chunkSize = 64 * 1024;
|
||||
const readError = 'Error while reading';
|
||||
function toReadable(value: string, throwError?: boolean): Readable {
|
||||
const totalChunks = Math.ceil(value.length / chunkSize);
|
||||
const stringChunks: string[] = [];
|
||||
|
||||
for (let i = 0, j = 0; i < totalChunks; ++i, j += chunkSize) {
|
||||
stringChunks[i] = value.substr(j, chunkSize);
|
||||
}
|
||||
|
||||
let counter = 0;
|
||||
return new Readable({
|
||||
read: function () {
|
||||
if (throwError) {
|
||||
this.emit('error', new Error(readError));
|
||||
}
|
||||
|
||||
let res!: string;
|
||||
let canPush = true;
|
||||
while (canPush && (res = stringChunks[counter++])) {
|
||||
canPush = this.push(res);
|
||||
}
|
||||
|
||||
// EOS
|
||||
if (!res) {
|
||||
this.push(null);
|
||||
}
|
||||
},
|
||||
encoding: 'utf8'
|
||||
});
|
||||
}
|
||||
|
||||
suite('PFS', function () {
|
||||
|
||||
// Given issues such as https://github.com/microsoft/vscode/issues/84066
|
||||
@@ -334,7 +301,7 @@ suite('PFS', function () {
|
||||
|
||||
test('stat link', async () => {
|
||||
if (isWindows) {
|
||||
return Promise.resolve(); // Symlinks are not the same on win, and we can not create them programitically without admin privileges
|
||||
return; // Symlinks are not the same on win, and we can not create them programitically without admin privileges
|
||||
}
|
||||
|
||||
const id1 = uuid.generateUuid();
|
||||
@@ -349,14 +316,38 @@ suite('PFS', function () {
|
||||
fs.symlinkSync(directory, symbolicLink);
|
||||
|
||||
let statAndIsLink = await pfs.statLink(directory);
|
||||
assert.ok(!statAndIsLink!.isSymbolicLink);
|
||||
assert.ok(!statAndIsLink?.symbolicLink);
|
||||
|
||||
statAndIsLink = await pfs.statLink(symbolicLink);
|
||||
assert.ok(statAndIsLink!.isSymbolicLink);
|
||||
assert.ok(statAndIsLink?.symbolicLink);
|
||||
assert.ok(!statAndIsLink?.symbolicLink?.dangling);
|
||||
|
||||
pfs.rimrafSync(directory);
|
||||
});
|
||||
|
||||
test('stat link (non existing target)', async () => {
|
||||
if (isWindows) {
|
||||
return; // Symlinks are not the same on win, and we can not create them programitically without admin privileges
|
||||
}
|
||||
|
||||
const id1 = uuid.generateUuid();
|
||||
const parentDir = path.join(os.tmpdir(), 'vsctests', id1);
|
||||
const directory = path.join(parentDir, 'pfs', id1);
|
||||
|
||||
const id2 = uuid.generateUuid();
|
||||
const symbolicLink = path.join(parentDir, 'pfs', id2);
|
||||
|
||||
await pfs.mkdirp(directory, 493);
|
||||
|
||||
fs.symlinkSync(directory, symbolicLink);
|
||||
|
||||
pfs.rimrafSync(directory);
|
||||
|
||||
const statAndIsLink = await pfs.statLink(symbolicLink);
|
||||
assert.ok(statAndIsLink?.symbolicLink);
|
||||
assert.ok(statAndIsLink?.symbolicLink?.dangling);
|
||||
});
|
||||
|
||||
test('readdir', async () => {
|
||||
if (canNormalize && typeof process.versions['electron'] !== 'undefined' /* needs electron */) {
|
||||
const id = uuid.generateUuid();
|
||||
@@ -420,17 +411,10 @@ suite('PFS', function () {
|
||||
return testWriteFileAndFlush(VSBuffer.fromString(smallData).buffer, smallData, VSBuffer.fromString(bigData).buffer, bigData);
|
||||
});
|
||||
|
||||
test('writeFile (stream)', async () => {
|
||||
const smallData = 'Hello World';
|
||||
const bigData = (new Array(100 * 1024)).join('Large String\n');
|
||||
|
||||
return testWriteFileAndFlush(toReadable(smallData), smallData, toReadable(bigData), bigData);
|
||||
});
|
||||
|
||||
async function testWriteFileAndFlush(
|
||||
smallData: string | Buffer | NodeJS.ReadableStream | Uint8Array,
|
||||
smallData: string | Buffer | Uint8Array,
|
||||
smallDataValue: string,
|
||||
bigData: string | Buffer | NodeJS.ReadableStream | Uint8Array,
|
||||
bigData: string | Buffer | Uint8Array,
|
||||
bigDataValue: string
|
||||
): Promise<void> {
|
||||
const id = uuid.generateUuid();
|
||||
@@ -450,22 +434,6 @@ suite('PFS', function () {
|
||||
await pfs.rimraf(parentDir);
|
||||
}
|
||||
|
||||
test('writeFile (file stream)', async () => {
|
||||
const id = uuid.generateUuid();
|
||||
const parentDir = path.join(os.tmpdir(), 'vsctests', id);
|
||||
const sourceFile = getPathFromAmdModule(require, './fixtures/index.html');
|
||||
const newDir = path.join(parentDir, 'pfs', id);
|
||||
const testFile = path.join(newDir, 'flushed.txt');
|
||||
|
||||
await pfs.mkdirp(newDir, 493);
|
||||
assert.ok(fs.existsSync(newDir));
|
||||
|
||||
await pfs.writeFile(testFile, fs.createReadStream(sourceFile));
|
||||
assert.equal(fs.readFileSync(testFile).toString(), fs.readFileSync(sourceFile).toString());
|
||||
|
||||
await pfs.rimraf(parentDir);
|
||||
});
|
||||
|
||||
test('writeFile (string, error handling)', async () => {
|
||||
const id = uuid.generateUuid();
|
||||
const parentDir = path.join(os.tmpdir(), 'vsctests', id);
|
||||
@@ -490,118 +458,6 @@ suite('PFS', function () {
|
||||
await pfs.rimraf(parentDir);
|
||||
});
|
||||
|
||||
test('writeFile (stream, error handling EISDIR)', async () => {
|
||||
const id = uuid.generateUuid();
|
||||
const parentDir = path.join(os.tmpdir(), 'vsctests', id);
|
||||
const newDir = path.join(parentDir, 'pfs', id);
|
||||
const testFile = path.join(newDir, 'flushed.txt');
|
||||
|
||||
await pfs.mkdirp(newDir, 493);
|
||||
|
||||
assert.ok(fs.existsSync(newDir));
|
||||
|
||||
fs.mkdirSync(testFile); // this will trigger an error because testFile is now a directory!
|
||||
|
||||
const readable = toReadable('Hello World');
|
||||
|
||||
let expectedError: Error | undefined;
|
||||
try {
|
||||
await pfs.writeFile(testFile, readable);
|
||||
} catch (error) {
|
||||
expectedError = error;
|
||||
}
|
||||
|
||||
if (!expectedError || (<any>expectedError).code !== 'EISDIR') {
|
||||
throw new Error('Expected EISDIR error for writing to folder but got: ' + (expectedError ? (<any>expectedError).code : 'no error'));
|
||||
}
|
||||
|
||||
// verify that the stream is still consumable (for https://github.com/Microsoft/vscode/issues/42542)
|
||||
assert.equal(readable.read(), 'Hello World');
|
||||
|
||||
await pfs.rimraf(parentDir);
|
||||
});
|
||||
|
||||
test('writeFile (stream, error handling READERROR)', async () => {
|
||||
const id = uuid.generateUuid();
|
||||
const parentDir = path.join(os.tmpdir(), 'vsctests', id);
|
||||
const newDir = path.join(parentDir, 'pfs', id);
|
||||
const testFile = path.join(newDir, 'flushed.txt');
|
||||
|
||||
await pfs.mkdirp(newDir, 493);
|
||||
assert.ok(fs.existsSync(newDir));
|
||||
|
||||
let expectedError: Error | undefined;
|
||||
try {
|
||||
await pfs.writeFile(testFile, toReadable('Hello World', true /* throw error */));
|
||||
} catch (error) {
|
||||
expectedError = error;
|
||||
}
|
||||
|
||||
if (!expectedError || expectedError.message !== readError) {
|
||||
throw new Error('Expected error for writing to folder');
|
||||
}
|
||||
|
||||
await pfs.rimraf(parentDir);
|
||||
});
|
||||
|
||||
test('writeFile (stream, error handling EACCES)', async () => {
|
||||
if (isLinux) {
|
||||
return Promise.resolve(); // somehow this test fails on Linux in our TFS builds
|
||||
}
|
||||
|
||||
const id = uuid.generateUuid();
|
||||
const parentDir = path.join(os.tmpdir(), 'vsctests', id);
|
||||
const newDir = path.join(parentDir, 'pfs', id);
|
||||
const testFile = path.join(newDir, 'flushed.txt');
|
||||
|
||||
await pfs.mkdirp(newDir, 493);
|
||||
|
||||
assert.ok(fs.existsSync(newDir));
|
||||
|
||||
fs.writeFileSync(testFile, '');
|
||||
fs.chmodSync(testFile, 33060); // make readonly
|
||||
|
||||
let expectedError: Error | undefined;
|
||||
try {
|
||||
await pfs.writeFile(testFile, toReadable('Hello World'));
|
||||
} catch (error) {
|
||||
expectedError = error;
|
||||
}
|
||||
|
||||
if (!expectedError || !((<any>expectedError).code !== 'EACCES' || (<any>expectedError).code !== 'EPERM')) {
|
||||
throw new Error('Expected EACCES/EPERM error for writing to folder but got: ' + (expectedError ? (<any>expectedError).code : 'no error'));
|
||||
}
|
||||
|
||||
await pfs.rimraf(parentDir);
|
||||
});
|
||||
|
||||
test('writeFile (file stream, error handling)', async () => {
|
||||
const id = uuid.generateUuid();
|
||||
const parentDir = path.join(os.tmpdir(), 'vsctests', id);
|
||||
const sourceFile = getPathFromAmdModule(require, './fixtures/index.html');
|
||||
const newDir = path.join(parentDir, 'pfs', id);
|
||||
const testFile = path.join(newDir, 'flushed.txt');
|
||||
|
||||
await pfs.mkdirp(newDir, 493);
|
||||
|
||||
assert.ok(fs.existsSync(newDir));
|
||||
|
||||
fs.mkdirSync(testFile); // this will trigger an error because testFile is now a directory!
|
||||
|
||||
let expectedError: Error | undefined;
|
||||
try {
|
||||
await pfs.writeFile(testFile, fs.createReadStream(sourceFile));
|
||||
} catch (error) {
|
||||
expectedError = error;
|
||||
}
|
||||
|
||||
if (!expectedError) {
|
||||
throw new Error('Expected error for writing to folder');
|
||||
}
|
||||
|
||||
await pfs.rimraf(parentDir);
|
||||
});
|
||||
|
||||
test('writeFileSync', async () => {
|
||||
const id = uuid.generateUuid();
|
||||
const parentDir = path.join(os.tmpdir(), 'vsctests', id);
|
||||
|
||||
Reference in New Issue
Block a user