File Explorer Plugin
A full-featured file explorer that displays your workspace files in a tree view with file operations like create, delete, and rename.
This example demonstrates tree views, tree data providers, file system operations, and custom UI components.
Features Demonstrated
- ✅ Tree view registration
- ✅ Tree data provider implementation
- ✅ File system operations (read, create, delete, rename)
- ✅ Context menus
- ✅ Icons for different file types
- ✅ Refresh functionality
- ✅ User input dialogs
Project Structure
- package.json
- manifest.json
- index.js
- FileTreeProvider.js
- icons.js
- README.md
Complete Source Code
package.json
{
"name": "lokus-file-explorer",
"version": "1.0.0",
"description": "File tree view for Lokus workspace",
"main": "index.js",
"keywords": ["lokus", "plugin", "file-explorer", "tree-view"],
"author": "Your Name",
"license": "MIT",
"engines": {
"lokus": "^1.0.0"
},
"dependencies": {
"@lokus/plugin-sdk": "^1.0.0"
}
}manifest.json
{
"id": "lokus-file-explorer",
"name": "File Explorer",
"version": "1.0.0",
"description": "Browse and manage workspace files in a tree view",
"author": "Your Name",
"main": "index.js",
"activationEvents": [
"onStartup"
],
"contributes": {
"commands": [
{
"id": "file-explorer.refresh",
"title": "Refresh File Explorer",
"category": "File Explorer",
"icon": "refresh"
},
{
"id": "file-explorer.newFile",
"title": "New File",
"category": "File Explorer",
"icon": "file-plus"
},
{
"id": "file-explorer.newFolder",
"title": "New Folder",
"category": "File Explorer",
"icon": "folder-plus"
},
{
"id": "file-explorer.delete",
"title": "Delete",
"category": "File Explorer",
"icon": "trash"
},
{
"id": "file-explorer.rename",
"title": "Rename",
"category": "File Explorer",
"icon": "edit"
}
],
"views": {
"explorer": [
{
"id": "fileExplorer",
"name": "Files",
"icon": "folder"
}
]
}
},
"permissions": [
"workspace:read",
"workspace:write"
]
}icons.js
/**
* Icon mappings for different file types
*/
const extensionIcons = {
// Documents
'md': 'file-text',
'txt': 'file-text',
'pdf': 'file-text',
// Code
'js': 'file-code',
'ts': 'file-code',
'jsx': 'file-code',
'tsx': 'file-code',
'json': 'file-code',
'html': 'file-code',
'css': 'file-code',
'py': 'file-code',
'rb': 'file-code',
'go': 'file-code',
'rs': 'file-code',
// Images
'png': 'image',
'jpg': 'image',
'jpeg': 'image',
'gif': 'image',
'svg': 'image',
'webp': 'image',
// Archives
'zip': 'archive',
'tar': 'archive',
'gz': 'archive',
'rar': 'archive',
// Config
'yaml': 'settings',
'yml': 'settings',
'toml': 'settings',
'ini': 'settings',
'env': 'settings',
};
/**
* Get icon for a file based on its extension
* @param {string} fileName - Name of the file
* @param {boolean} isDirectory - Whether it's a directory
* @returns {string} Icon name
*/
export function getFileIcon(fileName, isDirectory) {
if (isDirectory) {
return 'folder';
}
const extension = fileName.split('.').pop()?.toLowerCase();
return extensionIcons[extension] || 'file';
}
/**
* Check if a file should be hidden
* @param {string} fileName - Name of the file
* @returns {boolean} True if file should be hidden
*/
export function shouldHideFile(fileName) {
const hiddenPatterns = [
/^\./, // Hidden files (.git, .env, etc)
/^node_modules$/,
/^dist$/,
/^build$/,
/\.log$/,
];
return hiddenPatterns.some(pattern => pattern.test(fileName));
}FileTreeProvider.js
/**
* File Tree Data Provider
* Provides data for the file tree view
*/
import { getFileIcon, shouldHideFile } from './icons.js';
export class FileTreeProvider {
constructor(workspace) {
this.workspace = workspace;
this._onDidChangeTreeData = [];
}
/**
* Register a listener for tree data changes
* @param {Function} listener - Callback when tree data changes
* @returns {Object} Disposable to unregister the listener
*/
onDidChangeTreeData(listener) {
this._onDidChangeTreeData.push(listener);
return {
dispose: () => {
const index = this._onDidChangeTreeData.indexOf(listener);
if (index > -1) {
this._onDidChangeTreeData.splice(index, 1);
}
}
};
}
/**
* Refresh the tree view
* @param {any} element - Element to refresh (undefined = refresh all)
*/
refresh(element) {
this._onDidChangeTreeData.forEach(listener => listener(element));
}
/**
* Get children for a tree element
* @param {Object} element - Parent element or undefined for root
* @returns {Promise<Array>} Array of child elements
*/
async getChildren(element) {
if (!this.workspace.workspaceFolders || this.workspace.workspaceFolders.length === 0) {
return [];
}
// Root level - return workspace folders
if (!element) {
const rootPath = this.workspace.workspaceFolders[0].uri.path;
return this._readDirectory(rootPath);
}
// Get children of a directory
if (element.isDirectory) {
return this._readDirectory(element.path);
}
return [];
}
/**
* Get tree item representation for an element
* @param {Object} element - Element to get tree item for
* @returns {Object} Tree item configuration
*/
getTreeItem(element) {
return {
label: element.name,
collapsibleState: element.isDirectory ? 1 : 0, // 0=none, 1=collapsed, 2=expanded
icon: element.icon,
tooltip: element.path,
command: element.isDirectory ? undefined : {
id: 'workbench.action.openFile',
arguments: [element.path]
},
contextValue: element.isDirectory ? 'folder' : 'file'
};
}
/**
* Read directory and return file/folder objects
* @param {string} dirPath - Directory path to read
* @returns {Promise<Array>} Array of file/folder objects
*/
async _readDirectory(dirPath) {
try {
// Use Tauri's file system API
const { readDir } = await import('@tauri-apps/plugin-fs');
const entries = await readDir(dirPath);
// Filter and map entries
const items = entries
.filter(entry => !shouldHideFile(entry.name))
.map(entry => ({
name: entry.name,
path: `${dirPath}/$\\{entry.name\\}`,
isDirectory: entry.isDirectory,
icon: getFileIcon(entry.name, entry.isDirectory)
}))
.sort((a, b) => {
// Directories first, then alphabetically
if (a.isDirectory !== b.isDirectory) {
return a.isDirectory ? -1 : 1;
}
return a.name.localeCompare(b.name);
});
return items;
} catch (error) {
console.error('Error reading directory:', error);
return [];
}
}
}index.js
/**
* File Explorer Plugin for Lokus
*
* Provides a tree view for browsing workspace files
*/
import { FileTreeProvider } from './FileTreeProvider.js';
/**
* Called when the plugin is activated
*/
export function activate(api) {
console.log('File Explorer plugin activated');
// Check if workspace is available
if (!api.workspace.workspaceFolders || api.workspace.workspaceFolders.length === 0) {
api.ui.showInformationMessage('No workspace folder open. File Explorer requires an open workspace.');
return;
}
// Create tree data provider
const treeProvider = new FileTreeProvider(api.workspace);
// Register tree view
const treeDisposable = api.ui.registerTreeDataProvider('fileExplorer', treeProvider, {
title: 'Files'
});
// Track selected item for context commands
let selectedItem = null;
// Register refresh command
const refreshCommand = api.commands.register({
id: 'file-explorer.refresh',
title: 'Refresh File Explorer',
execute: () => {
treeProvider.refresh();
api.ui.showInformationMessage('File explorer refreshed');
}
});
// Register new file command
const newFileCommand = api.commands.register({
id: 'file-explorer.newFile',
title: 'New File',
execute: async (item) => {
try {
// Get parent directory
const parentDir = item?.isDirectory ? item.path : getParentDir(item?.path);
if (!parentDir) {
api.ui.showErrorMessage('Could not determine parent directory');
return;
}
// Prompt for file name
const fileName = await api.ui.showInputBox({
prompt: 'Enter file name',
placeholder: 'example.md'
});
if (!fileName) return;
// Create the file
const { writeTextFile } = await import('@tauri-apps/plugin-fs');
const filePath = `${parentDir}/$\\{fileName\\}`;
await writeTextFile(filePath, '');
// Refresh tree
treeProvider.refresh();
api.ui.showInformationMessage(`Created $\\{fileName\\}`);
} catch (error) {
api.ui.showErrorMessage(`Failed to create file: $\\{error.message\\}`);
}
}
});
// Register new folder command
const newFolderCommand = api.commands.register({
id: 'file-explorer.newFolder',
title: 'New Folder',
execute: async (item) => {
try {
const parentDir = item?.isDirectory ? item.path : getParentDir(item?.path);
if (!parentDir) {
api.ui.showErrorMessage('Could not determine parent directory');
return;
}
const folderName = await api.ui.showInputBox({
prompt: 'Enter folder name',
placeholder: 'my-folder'
});
if (!folderName) return;
const { mkdir } = await import('@tauri-apps/plugin-fs');
const folderPath = `${parentDir}/$\\{folderName\\}`;
await mkdir(folderPath);
treeProvider.refresh();
api.ui.showInformationMessage(`Created folder $\\{folderName\\}`);
} catch (error) {
api.ui.showErrorMessage(`Failed to create folder: $\\{error.message\\}`);
}
}
});
// Register delete command
const deleteCommand = api.commands.register({
id: 'file-explorer.delete',
title: 'Delete',
execute: async (item) => {
if (!item) {
api.ui.showWarningMessage('No file or folder selected');
return;
}
try {
// Confirm deletion
const confirmed = await api.ui.showConfirm({
title: 'Delete',
message: `Are you sure you want to delete ${item.name}?`,
confirmText: 'Delete',
cancelText: 'Cancel'
});
if (!confirmed) return;
// Delete the file/folder
const { remove } = await import('@tauri-apps/plugin-fs');
await remove(item.path, { recursive: item.isDirectory });
treeProvider.refresh();
api.ui.showInformationMessage(`Deleted $\\{item.name\\}`);
} catch (error) {
api.ui.showErrorMessage(`Failed to delete: $\\{error.message\\}`);
}
}
});
// Register rename command
const renameCommand = api.commands.register({
id: 'file-explorer.rename',
title: 'Rename',
execute: async (item) => {
if (!item) {
api.ui.showWarningMessage('No file or folder selected');
return;
}
try {
const newName = await api.ui.showInputBox({
prompt: 'Enter new name',
placeholder: item.name,
value: item.name
});
if (!newName || newName === item.name) return;
const { rename } = await import('@tauri-apps/plugin-fs');
const parentDir = getParentDir(item.path);
const newPath = `${parentDir}/$\\{newName\\}`;
await rename(item.path, newPath);
treeProvider.refresh();
api.ui.showInformationMessage(`Renamed to $\\{newName\\}`);
} catch (error) {
api.ui.showErrorMessage(`Failed to rename: $\\{error.message\\}`);
}
}
});
// Helper function to get parent directory
function getParentDir(path) {
if (!path) return api.workspace.workspaceFolders?.[0]?.uri?.path;
const parts = path.split('/');
parts.pop();
return parts.join('/');
}
// Return cleanup function
return {
dispose: () => {
treeDisposable.dispose();
refreshCommand.dispose();
newFileCommand.dispose();
newFolderCommand.dispose();
deleteCommand.dispose();
renameCommand.dispose();
console.log('File Explorer plugin deactivated');
}
};
}
/**
* Called when the plugin is deactivated
*/
export function deactivate() {
// Additional cleanup if needed
}Installation & Testing
Step 1: Create Plugin
mkdir lokus-file-explorer
cd lokus-file-explorer
# Copy all files
npm installStep 2: Link and Enable
npm link
# In Lokus plugins directory
npm link lokus-file-explorerStep 3: Test Features
- Open a workspace folder in Lokus
- Enable the File Explorer plugin
- You should see a “Files” tree view in the sidebar
- Try these operations:
- Click files to open them
- Right-click to see context menu
- Create new files/folders
- Rename items
- Delete items
- Refresh the view
Code Walkthrough
Tree Data Provider
The tree data provider is the core of tree views:
class FileTreeProvider {
// Called when tree needs to be refreshed
onDidChangeTreeData(listener) { }
// Returns children for an element
async getChildren(element) { }
// Returns tree item configuration
getTreeItem(element) { }
}Registering the Tree View
api.ui.registerTreeDataProvider('fileExplorer', treeProvider, {
title: 'Files'
});The first parameter must match the id in manifest.json views contribution.
Tree Item Configuration
{
label: 'file.js', // Display name
collapsibleState: 1, // 0=none, 1=collapsed, 2=expanded
icon: 'file-code', // Icon name
tooltip: '/path/to/file.js', // Hover text
command: { // Command to run on click
id: 'workbench.action.openFile',
arguments: ['/path/to/file.js']
},
contextValue: 'file' // For context menu filtering
}File System Operations
Using Tauri’s file system plugin:
// Read directory
const { readDir } = await import('@tauri-apps/plugin-fs');
const entries = await readDir(dirPath);
// Create file
const { writeTextFile } = await import('@tauri-apps/plugin-fs');
await writeTextFile(filePath, content);
// Create folder
const { mkdir } = await import('@tauri-apps/plugin-fs');
await mkdir(folderPath);
// Delete
const { remove } = await import('@tauri-apps/plugin-fs');
await remove(path, { recursive: true });
// Rename
const { rename } = await import('@tauri-apps/plugin-fs');
await rename(oldPath, newPath);Refreshing the Tree
treeProvider.refresh(); // Refresh entire tree
treeProvider.refresh(element); // Refresh specific elementThis triggers onDidChangeTreeData listeners, causing the tree to reload.
Extension Ideas
Add more features:
- File Search - Search within tree
- Drag & Drop - Move files by dragging
- Copy/Paste - File clipboard operations
- Icons by Type - More detailed file type icons
- Git Status - Show git status in tree
- File Watchers - Auto-refresh on changes
Example: Git Status
async function getGitStatus(filePath) {
const { Command } = await import('@tauri-apps/plugin-shell');
const result = await Command.create('git', ['status', '--porcelain', filePath]).execute();
if (result.stdout.startsWith('M ')) return 'modified';
if (result.stdout.startsWith('A ')) return 'added';
if (result.stdout.startsWith('D ')) return 'deleted';
return 'unmodified';
}
// In getTreeItem:
const gitStatus = await getGitStatus(element.path);
return {
label: element.name,
description: gitStatus === 'modified' ? 'M' : undefined,
// ... rest of config
};Performance Tips
For large directories:
- Lazy Loading - Only load children when expanded
- Virtual Scrolling - Tree view handles this automatically
- Caching - Cache directory contents
- Filtering - Hide node_modules, .git, etc.
// Cache implementation
const cache = new Map();
async function _readDirectory(dirPath) {
if (cache.has(dirPath)) {
return cache.get(dirPath);
}
const entries = await readDir(dirPath);
cache.set(dirPath, entries);
return entries;
}Common Issues
Issue: Tree view doesn’t appear
Solution: Make sure view ID in registerTreeDataProvider matches the ID in manifest.json
Issue: Files don’t open on click
Add a command to handle file opening:
command: {
id: 'file-explorer.openFile',
arguments: [element.path]
}Issue: Tree doesn’t refresh after operations
Always call treeProvider.refresh() after file operations.
Next Steps
- AI Assistant - Network requests and streaming
- UI API Reference - Complete UI API docs
- Tree View Guide - Advanced tree view patterns
Ready for advanced topics? Continue to AI Assistant Example for network integration.