mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-24 20:26:08 +00:00
data tree: first steps
This commit is contained in:
@@ -15,13 +15,13 @@ import { ITreeModel, ITreeNode } from 'vs/base/browser/ui/tree/tree';
|
||||
import { ISpliceable } from 'vs/base/common/sequence';
|
||||
import { IIndexTreeModelOptions } from 'vs/base/browser/ui/tree/indexTreeModel';
|
||||
|
||||
function toTreeListOptions<T>(options?: IListOptions<T>): IListOptions<ITreeNode<T, any>> {
|
||||
export function createComposedTreeListOptions<T, N extends { element: T }>(options?: IListOptions<T>): IListOptions<N> {
|
||||
if (!options) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let identityProvider: IIdentityProvider<ITreeNode<T, any>> | undefined = undefined;
|
||||
let multipleSelectionController: IMultipleSelectionController<ITreeNode<T, any>> | undefined = undefined;
|
||||
let identityProvider: IIdentityProvider<N> | undefined = undefined;
|
||||
let multipleSelectionController: IMultipleSelectionController<N> | undefined = undefined;
|
||||
|
||||
if (options.identityProvider) {
|
||||
identityProvider = el => options.identityProvider(el.element);
|
||||
@@ -45,15 +45,15 @@ function toTreeListOptions<T>(options?: IListOptions<T>): IListOptions<ITreeNode
|
||||
};
|
||||
}
|
||||
|
||||
class TreeDelegate<T> implements IVirtualDelegate<ITreeNode<T, any>> {
|
||||
export class ComposedTreeDelegate<T, N extends { element: T }> implements IVirtualDelegate<N> {
|
||||
|
||||
constructor(private delegate: IVirtualDelegate<T>) { }
|
||||
|
||||
getHeight(element: ITreeNode<T, any>): number {
|
||||
getHeight(element: N): number {
|
||||
return this.delegate.getHeight(element.element);
|
||||
}
|
||||
|
||||
getTemplateId(element: ITreeNode<T, any>): string {
|
||||
getTemplateId(element: N): string {
|
||||
return this.delegate.getTemplateId(element.element);
|
||||
}
|
||||
}
|
||||
@@ -149,13 +149,13 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
|
||||
renderers: IRenderer<T, any>[],
|
||||
options?: ITreeOptions<T, TFilterData>
|
||||
) {
|
||||
const treeDelegate = new TreeDelegate(delegate);
|
||||
const treeDelegate = new ComposedTreeDelegate<T, ITreeNode<T, TFilterData>>(delegate);
|
||||
|
||||
const onDidChangeCollapseStateRelay = new Relay<ITreeNode<T, TFilterData>>();
|
||||
const treeRenderers = renderers.map(r => new TreeRenderer(r, onDidChangeCollapseStateRelay.event));
|
||||
const treeRenderers = renderers.map(r => new TreeRenderer<T, TFilterData, any>(r, onDidChangeCollapseStateRelay.event));
|
||||
this.disposables.push(...treeRenderers);
|
||||
|
||||
this.view = new List(container, treeDelegate, treeRenderers, toTreeListOptions(options));
|
||||
this.view = new List(container, treeDelegate, treeRenderers, createComposedTreeListOptions<T, ITreeNode<T, TFilterData>>(options));
|
||||
this.model = this.createModel(this.view, options);
|
||||
onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState;
|
||||
|
||||
|
||||
132
src/vs/base/browser/ui/tree/dataTree.ts
Normal file
132
src/vs/base/browser/ui/tree/dataTree.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ITreeOptions, ComposedTreeDelegate, createComposedTreeListOptions } from 'vs/base/browser/ui/tree/abstractTree';
|
||||
import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree';
|
||||
import { IVirtualDelegate, IRenderer } from 'vs/base/browser/ui/list/list';
|
||||
import { ITreeElement } from 'vs/base/browser/ui/tree/tree';
|
||||
|
||||
export interface IDataTreeElement<T> {
|
||||
readonly element: T;
|
||||
readonly collapsible?: boolean;
|
||||
readonly collapsed?: boolean;
|
||||
}
|
||||
|
||||
export interface IDataSource<T extends NonNullable<any>> {
|
||||
hasChildren(element: T | null): boolean;
|
||||
getChildren(element: T | null): Thenable<IDataTreeElement<T>[]>;
|
||||
}
|
||||
|
||||
enum DataTreeNodeState {
|
||||
Idle,
|
||||
Loading
|
||||
}
|
||||
|
||||
interface IDataTreeNode<T extends NonNullable<any>> {
|
||||
readonly element: T;
|
||||
readonly parent: IDataTreeNode<T> | null;
|
||||
state: DataTreeNodeState;
|
||||
// promise: Thenable<any>;
|
||||
}
|
||||
|
||||
interface IDataTreeListTemplateData<T> {
|
||||
templateData: T;
|
||||
}
|
||||
|
||||
class DataTreeRenderer<T, TTemplateData> implements IRenderer<IDataTreeNode<T>, IDataTreeListTemplateData<TTemplateData>> {
|
||||
|
||||
readonly templateId: string;
|
||||
|
||||
constructor(private renderer: IRenderer<T, TTemplateData>) {
|
||||
this.templateId = renderer.templateId;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): IDataTreeListTemplateData<TTemplateData> {
|
||||
const templateData = this.renderer.renderTemplate(container);
|
||||
|
||||
return { templateData };
|
||||
}
|
||||
|
||||
renderElement(node: IDataTreeNode<T>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>): void {
|
||||
this.renderer.renderElement(node.element, index, templateData.templateData);
|
||||
}
|
||||
|
||||
disposeElement(node: IDataTreeNode<T>): void {
|
||||
// noop
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: IDataTreeListTemplateData<TTemplateData>): void {
|
||||
this.renderer.disposeTemplate(templateData.templateData);
|
||||
}
|
||||
}
|
||||
|
||||
export class DataTree<T extends NonNullable<any>, TFilterData = void> {
|
||||
|
||||
private tree: ObjectTree<IDataTreeNode<T>, TFilterData>;
|
||||
private root: IDataTreeNode<T>;
|
||||
private nodes = new Map<T, IDataTreeNode<T>>();
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
delegate: IVirtualDelegate<T>,
|
||||
renderers: IRenderer<T, any>[],
|
||||
private dataSource: IDataSource<T>,
|
||||
options?: ITreeOptions<T, TFilterData>
|
||||
) {
|
||||
const treeDelegate = new ComposedTreeDelegate<T, IDataTreeNode<T>>(delegate);
|
||||
const treeRenderers = renderers.map(r => new DataTreeRenderer(r));
|
||||
const treeOptions = createComposedTreeListOptions<T, IDataTreeNode<T>>(options);
|
||||
|
||||
this.tree = new ObjectTree(container, treeDelegate, treeRenderers, treeOptions);
|
||||
this.root = {
|
||||
element: undefined, // TODO@joao
|
||||
parent: null,
|
||||
state: DataTreeNodeState.Idle,
|
||||
};
|
||||
|
||||
this.nodes.set(null, this.root);
|
||||
}
|
||||
|
||||
refresh(element: T | null, recursive = true): Thenable<void> {
|
||||
const node: IDataTreeNode<T> = this.nodes.get(element);
|
||||
|
||||
if (typeof node === 'undefined') {
|
||||
throw new Error(`Data tree node not found: ${element}`);
|
||||
}
|
||||
|
||||
const hasChildren = this.dataSource.hasChildren(element);
|
||||
|
||||
if (!hasChildren) {
|
||||
this.tree.setChildren(node === this.root ? null : node);
|
||||
return Promise.resolve(null);
|
||||
} else {
|
||||
node.state = DataTreeNodeState.Loading;
|
||||
|
||||
return this.dataSource.getChildren(element)
|
||||
.then(children => {
|
||||
node.state = DataTreeNodeState.Idle;
|
||||
|
||||
const createTreeElement = (el: IDataTreeElement<T>): ITreeElement<IDataTreeNode<T>> => {
|
||||
return {
|
||||
element: {
|
||||
element: el.element,
|
||||
state: DataTreeNodeState.Idle,
|
||||
parent: node
|
||||
},
|
||||
collapsible: el.collapsible,
|
||||
collapsed: typeof el.collapsed === 'boolean' ? el.collapsed : true
|
||||
};
|
||||
};
|
||||
|
||||
const nodeChildren = children.map<ITreeElement<IDataTreeNode<T>>>(createTreeElement);
|
||||
|
||||
this.tree.setChildren(node === this.root ? null : node, nodeChildren);
|
||||
}, err => {
|
||||
node.state = DataTreeNodeState.Idle;
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { ISpliceable } from 'vs/base/common/sequence';
|
||||
import { ITreeNode, ITreeModel, ITreeElement } from 'vs/base/browser/ui/tree/tree';
|
||||
import { ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel';
|
||||
|
||||
export class ObjectTree<T, TFilterData = void> extends AbstractTree<T, TFilterData, ITreeNode<T, TFilterData>> {
|
||||
export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends AbstractTree<T, TFilterData, ITreeNode<T, TFilterData>> {
|
||||
|
||||
protected model: ObjectTreeModel<T, TFilterData>;
|
||||
|
||||
|
||||
@@ -37,56 +37,118 @@
|
||||
|
||||
require.config({ baseUrl: '/static' });
|
||||
|
||||
require(['vs/base/browser/ui/tree/indexTree', 'vs/base/browser/ui/tree/tree', 'vs/base/common/iterator'], ({ IndexTree }, { TreeVisibility }, { iter }) => {
|
||||
const delegate = {
|
||||
getHeight() { return 22; },
|
||||
getTemplateId() { return 'template'; }
|
||||
};
|
||||
require(['vs/base/browser/ui/tree/indexTree', 'vs/base/browser/ui/tree/dataTree', 'vs/base/browser/ui/tree/tree', 'vs/base/common/iterator'], ({ IndexTree }, { DataTree }, { TreeVisibility }, { iter }) => {
|
||||
function createIndexTree() {
|
||||
const delegate = {
|
||||
getHeight() { return 22; },
|
||||
getTemplateId() { return 'template'; }
|
||||
};
|
||||
|
||||
const renderer = {
|
||||
templateId: 'template',
|
||||
renderTemplate(container) { return container; },
|
||||
renderElement(element, index, container) {
|
||||
container.textContent = element;
|
||||
},
|
||||
disposeTemplate() { }
|
||||
};
|
||||
const renderer = {
|
||||
templateId: 'template',
|
||||
renderTemplate(container) { return container; },
|
||||
renderElement(element, index, container) {
|
||||
container.textContent = element;
|
||||
},
|
||||
disposeTemplate() { }
|
||||
};
|
||||
|
||||
const treeFilter = new class {
|
||||
constructor() {
|
||||
this.pattern = null;
|
||||
let timeout;
|
||||
filter.oninput = () => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => this.updatePattern(), 300);
|
||||
};
|
||||
}
|
||||
updatePattern() {
|
||||
if (!filter.value) {
|
||||
const treeFilter = new class {
|
||||
constructor() {
|
||||
this.pattern = null;
|
||||
} else {
|
||||
this.pattern = new RegExp(filter.value, 'i');
|
||||
let timeout;
|
||||
filter.oninput = () => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => this.updatePattern(), 300);
|
||||
};
|
||||
}
|
||||
updatePattern() {
|
||||
if (!filter.value) {
|
||||
this.pattern = null;
|
||||
} else {
|
||||
this.pattern = new RegExp(filter.value, 'i');
|
||||
}
|
||||
|
||||
perf('refilter', () => tree.refilter());
|
||||
}
|
||||
filter(el) {
|
||||
return (this.pattern ? this.pattern.test(el) : true) ? TreeVisibility.Visible : TreeVisibility.Recurse;
|
||||
}
|
||||
};
|
||||
|
||||
const tree = new IndexTree(container, delegate, [renderer], { filter: treeFilter });
|
||||
|
||||
return { tree, treeFilter };
|
||||
}
|
||||
|
||||
function createDataTree() {
|
||||
const delegate = {
|
||||
getHeight() { return 22; },
|
||||
getTemplateId() { return 'template'; }
|
||||
};
|
||||
|
||||
const renderer = {
|
||||
templateId: 'template',
|
||||
renderTemplate(container) { return container; },
|
||||
renderElement(element, index, container) { container.textContent = element.name; },
|
||||
disposeTemplate() { }
|
||||
};
|
||||
|
||||
const treeFilter = new class {
|
||||
constructor() {
|
||||
this.pattern = null;
|
||||
let timeout;
|
||||
filter.oninput = () => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => this.updatePattern(), 300);
|
||||
};
|
||||
}
|
||||
|
||||
perf('refilter', () => tree.refilter());
|
||||
}
|
||||
filter(el) {
|
||||
return (this.pattern ? this.pattern.test(el) : true) ? TreeVisibility.Visible : TreeVisibility.Recurse;
|
||||
}
|
||||
};
|
||||
updatePattern() {
|
||||
if (!filter.value) {
|
||||
this.pattern = null;
|
||||
} else {
|
||||
this.pattern = new RegExp(filter.value, 'i');
|
||||
}
|
||||
|
||||
const tree = new IndexTree(container, delegate, [renderer], { filter: treeFilter });
|
||||
perf('refilter', () => tree.refilter());
|
||||
}
|
||||
filter(el) {
|
||||
return (this.pattern ? this.pattern.test(el.name) : true) ? TreeVisibility.Visible : TreeVisibility.Recurse;
|
||||
}
|
||||
};
|
||||
|
||||
function setModel(model) {
|
||||
performance.mark('before splice');
|
||||
const start = performance.now();
|
||||
;
|
||||
console.log('splice took', performance.now() - start);
|
||||
performance.mark('after splice');
|
||||
const dataSource = new class {
|
||||
hasChildren(element) {
|
||||
return element === null || element.type === 'dir';
|
||||
}
|
||||
getChildren(element) {
|
||||
return new Promise((c, e) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', element ? `/api/readdir?path=${element.path}` : '/api/readdir');
|
||||
xhr.send();
|
||||
xhr.onreadystatechange = function () {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
const els = JSON.parse(this.responseText).map(element => ({
|
||||
element,
|
||||
collapsible: element.type === 'dir'
|
||||
}));
|
||||
c(els);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const tree = new DataTree(container, delegate, [renderer], dataSource, { filter: treeFilter });
|
||||
|
||||
return { tree, treeFilter };
|
||||
}
|
||||
|
||||
switch (location.search) {
|
||||
case '?problems': {
|
||||
const { tree, treeFilter } = createIndexTree();
|
||||
|
||||
const files = [];
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
const errors = [];
|
||||
@@ -101,7 +163,16 @@
|
||||
perf('splice', () => tree.splice([0], 0, files));
|
||||
break;
|
||||
}
|
||||
case '?data': {
|
||||
const { tree, treeFilter } = createDataTree();
|
||||
|
||||
tree.refresh(null);
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
const { tree, treeFilter } = createIndexTree();
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '/api/ls?path=');
|
||||
xhr.send();
|
||||
|
||||
@@ -26,6 +26,29 @@ async function getTree(fsPath, level) {
|
||||
return { element, collapsible: true, collapsed: false, children };
|
||||
}
|
||||
|
||||
async function readdir(relativePath) {
|
||||
const absolutePath = relativePath ? path.join(root, relativePath) : root;
|
||||
const childNames = await fs.readdir(absolutePath);
|
||||
const childStats = await Promise.all(childNames.map(async name => await fs.stat(path.join(absolutePath, name))));
|
||||
const result = [];
|
||||
|
||||
for (let i = 0; i < childNames.length; i++) {
|
||||
const name = childNames[i];
|
||||
const path = relativePath ? `${relativePath}/${name}` : name;
|
||||
const stat = childStats[i];
|
||||
|
||||
if (stat.isFile()) {
|
||||
result.push({ type: 'file', name, path });
|
||||
} else if (!stat.isDirectory() || name === '.git' || name === '.build') {
|
||||
continue;
|
||||
} else {
|
||||
result.push({ type: 'dir', name, path });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
app.use(serve('public'));
|
||||
app.use(mount('/static', serve('../../out')));
|
||||
app.use(_.get('/api/ls', async ctx => {
|
||||
@@ -33,7 +56,13 @@ app.use(_.get('/api/ls', async ctx => {
|
||||
const absolutePath = path.join(root, relativePath);
|
||||
|
||||
ctx.body = await getTree(absolutePath, 0);
|
||||
}))
|
||||
}));
|
||||
|
||||
app.use(_.get('/api/readdir', async ctx => {
|
||||
const relativePath = ctx.query.path;
|
||||
|
||||
ctx.body = await readdir(relativePath);
|
||||
}));
|
||||
|
||||
app.listen(3000);
|
||||
console.log('http://localhost:3000');
|
||||
Reference in New Issue
Block a user