I found myself needing paging in a TreeView. Here's how I did it.
- I'm using JavaScript - sorry about that!
- My TreeView data is SQL driven.
Video: https://user-images.githubusercontent.com/3708366/137364458-bc9ad98e-b65d-46c9-9b01-7c32e90c44c6.mov
Here's the base for the TreeView class (extending TreeDataProvider). PAGE_SIZE is how much to load in each fetch.
const PAGE_SIZE = 100;
module.exports = class UserList {
/**
* @param {vscode.ExtensionContext} context
*/
constructor(context) {
this.emitter = new vscode.EventEmitter();
this.onDidChangeTreeData = this.emitter.event;
/** @type {{[key: string]: object[]}} */
this.cache = {};
}
refresh() {
this.emitter.fire();
}
}- The
cachefield is used to store TreeView data. - We use
refreshto reload the table from thecache - We pass in our extension context so we can register commands in the contructor specific to this TreeView
/**
* @param {vscode.TreeItem|UserType} [element]
* @returns {Promise<vscode.TreeItem[]>};
*/
async getChildren(element) {
let items = [], item;
// UserType extends TreeView and adds `type`
if (element) {
items = await this.fetchUsers(element.type, false);
items.push(moreButton(element.type));
} else {
items = [
new UserType({title: `Admins`, type: `admin`}),
new UserType({title: `Regular`, type: `regular`})
]
}
return items;
}getChildrenis a method on our classfetchUsersreturnsTreeItem[]. We will define this soonmoreButtonreturnsTreeItem
const moreButton = (type) => {
const item = new vscode.TreeItem(`More...`, vscode.TreeItemCollapsibleState.None);
item.iconPath = new vscode.ThemeIcon(`add`);
item.command = {
command: `myext.loadMoreUsers`,
title: `Load more`,
// @ts-ignore
arguments: [type]
};
return item;
}- We pass the type in so we know what type of users we need to fetch
- We use the type for the
this.cachekey - We implement our command
myext.loadMoreUserslater on
/**
* @param {"admins"|"regular"} type The type of users we want to fetch
* @param {boolean} [addRows] Passing false/null will returning the existing cache, but if it is empty will fetch the first page
*/
async fetchUsers(type, addRows) {
const key = type;
let offset;
// Only fetch the rows if we have none or are looking for the next page
if (addRows || this.cache[key] === undefined) {
// The offset is basically the lenfth of the cache
offset = (this.cache[key] ? this.cache[key].length : 0);
// Fetch the data from our source
const data = await Users.get(type, {
limit: PAGE_SIZE,
offset
});
if (data.length > 0) {
// Here, I am mapping to UserItem, which is type of `TreeView`
const items = data.map(item => new UserItem(item));
if (this.cache[key]) {
this.cache[key].push(...items);
} else {
this.cache[key] = items;
}
} else {
vscode.window.showInformationMessage(`No more items to load.`);
}
}
// Return a copy
return this.cache[key].slice(0);
}fetchUsers can do two things:
- Return just the existing cache if there is one, otherwise fetch the first page, add it to the cache and return that.
- Return the next page, add it to the cache and then return it
Our 'More' button calls the following command, which is defined in the constructor to make use of the extension context.
vscode.commands.registerCommand(`myext.loadMoreUsers`, async (type) => {
if (type) {
// Fetch the next page of data from the source
await this.fetchUsers(type, true);
// Refresh the TreeView, which collects data from the cache
this.refresh();
}
})What is important to note here is that because refresh invokes getChildren, which in return calls fetchUsers again. Luckily, the second time getChildren calls fetchUsers it won't reach out to the database. This is thanks to the addRows parameter on fetchUsers.