Skip to content

Commit

Permalink
Fix/FileTree (#24)
Browse files Browse the repository at this point in the history
Fixes issue #6
  • Loading branch information
FossPrime authored Jun 15, 2023
1 parent 2895972 commit 2a34a9c
Show file tree
Hide file tree
Showing 13 changed files with 229 additions and 52 deletions.
97 changes: 97 additions & 0 deletions .replit
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
hidden=[".config", ".gitignore", ".github", "node_modules", "pnpm-lock.yaml", "tsconfig.json", "tsconfig.node.json", "vite.config.ts"]

# onBoot=['echo', '$PATH'] # ⚠ node is not in env, yet
# onBoot=['echo', 'rebooted..'] # Runs on reboot, very limited ENV vars
# compile="npm i" # No runtime ENV vars
# run = ["npm", "run", "dev"] # Use TOML's """ for a multiline bash script
run = """
echo NodeJS Version: $(node --version) "\n"
pnpm run dev
bash --norc
""" # "

compile = """
pnpm i
"""

entrypoint = ".replit"

[[ports]]
localPort = 5101
remotePort = 80

[nix]
channel = "stable-22_11"

[env]
PATH = "/home/runner/$REPL_SLUG/.config/npm/node_global/bin:/home/runner/$REPL_SLUG/node_modules/.bin:./node_modules/.bin:/home/runner/$REPL_SLUG/.config/pnpm"
npm_config_prefix = "/home/runner/$REPL_SLUG/.config/npm/node_global" # Global install support
npm_config_yes="true" # This is a safe space, don't ask stupid questions
PNPM_HOME = "/home/runner/$REPL_SLUG/.config/pnpm"
VITE_HOST = "0.0.0.0"
# NODE_OPTIONS="--max_old_space_size=384"
# EDITOR="replit-git-editor" # Not reliable, use curl replspace instead
#NODE_NO_WARNINGS="1"

# Helper for Replit's git importer
[gitHubImport]
requiredFiles = ["package.json", "tsconfig.json", "pnpm-lock.yaml"]

# Disables UPM, which BREAKS with PNPM, NPM v9, PNPM/Turbo/Yarn/Deno/Bun etc
[packager]
language = "no" # nodejs-npm / nodejs-yarn
ignoredPaths = ["."] # disables guessImports

[languages.typescript]
pattern = "**/{*.ts,*.js,*.tsx,*.jsx}"
syntax = "typescript"

[languages.typescript.languageServer]
start = [ "typescript-language-server", "--stdio" ]

# CWD is not supported
# As a workaround, use Node 19 with --import and a helper script that CD's to a directory based on env vars
[debugger]
support = true

[debugger.interactive]
transport = "localhost:0"
startCommand = [ "dap-node" ]

[debugger.interactive.initializeMessage]
command = "initialize"
type = "request"

[debugger.interactive.initializeMessage.arguments]
clientID = "replit"
clientName = "replit.com"
columnsStartAt1 = true
linesStartAt1 = true
locale = "en-us"
pathFormat = "path"
supportsInvalidatedEvent = true
supportsProgressReporting = true
supportsRunInTerminalRequest = true
supportsVariablePaging = true
supportsVariableType = true

[debugger.interactive.launchMessage]
command = "launch"
type = "request"

[debugger.interactive.launchMessage.arguments]
runtimeArgs = ["--loader", "ts-node/esm/transpile-only"]
args = []
console = "externalTerminal"
cwd = "." # Broken
environment = [] # Broken
pauseForSourceMap = false
program = "index.ts"
request = "launch"
sourceMaps = true
stopOnEntry = false
type = "pwa-node"

[debugger.interactive.launchMessage.arguments.env]
IS_RAY_AWESOME = "yes"

8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@
"vite-plugin-node-polyfills": "^0.9.0",
"vite-plugin-rewrite-all": "^1.0.1"
},
"prettier": {
"printWidth": 100,
"semi": true,
"singleQuote": true,
"bracketSpacing": false,
"trailingComma": "all",
"arrowParens": "avoid"
},
"pnpm": {
"overrides": {
"dockview-core": "https://pkg.csb.dev/mathuo/dockview/commit/2156a1dd/dockview-core"
Expand Down
7 changes: 7 additions & 0 deletions replit.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{ pkgs }: { deps = with pkgs; [
less
bashInteractive
nodejs-18_x
nodePackages.typescript-language-server # Add nodePackages.typescript if not in node_modules
nodePackages.pnpm # Best of YARN 2, but as easy to run as NPM
]; }
80 changes: 40 additions & 40 deletions src/components/FileTree.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import {useRef, useEffect} from 'react';
import {Tree, UncontrolledTreeEnvironment} from 'react-complex-tree';
import {useRef, Ref} from 'react';
import {Tree, UncontrolledTreeEnvironment, TreeEnvironmentRef} from 'react-complex-tree';
import {EventEmitter} from 'react-complex-tree/src/EventEmitter';
import {getDirAsTree} from '../utils/webcontainer';
import {useDarkMode} from '../hooks/useDarkMode';
import Debug from '../utils/debug';
import { debounce } from '../utils/debounce';

import type * as RCT from 'react-complex-tree';
import type {FileSystemAPI} from '@webcontainer/api';

const debug = Debug('FileTree')

interface FileTreeProps {
fs: FileSystemAPI,
onRenameItem: (path: string, name: string) => void,
Expand All @@ -22,25 +26,33 @@ const root: RCT.TreeItem<string> = {
children: [],
};

export const FileTreeState = {
refresh: new Function(),
treeEnv: null as Ref<TreeEnvironmentRef<any, never>>
}

export function FileTree(props: FileTreeProps) {
const isDark = useDarkMode();
const UTreeEnvironment = useRef() as Ref<TreeEnvironmentRef<any, never>>
const provider = useRef<TreeProvider<string>>(new TreeProvider({root}));

const refresh = async () => {
const data = await getDirAsTree(props.fs, '.', 'root', root, {});
provider.current.updateItems(data);
const refresh = async (updateMessage?: string) => {
debug('refresh updateMessage', updateMessage);
const data = await getDirAsTree(props.fs, '.', 'root', Object.assign({}, root, {children: []}), {});
debug('refresh getDirAsTree', data);
provider.current.updateItems(data)
};

useEffect(() => {
refresh();
const i = setInterval(refresh, 1000);
return () => clearInterval(i);
}, []);

Object.assign(FileTreeState, {
refresh: debounce(refresh, 300),
treeEnv: UTreeEnvironment
})

return (
<div style={{overflow: 'scroll'}}>
<div className={isDark ? 'rct-dark' : 'rct-default'}>
<UncontrolledTreeEnvironment
ref={UTreeEnvironment}
canRename
canSearch
canDragAndDrop
Expand All @@ -50,10 +62,13 @@ export function FileTree(props: FileTreeProps) {
getItemTitle={item => item.data}
onPrimaryAction={item => props.onTriggerItem(item.index.toString(), item.data)}
onRenameItem={(item, name) => props.onRenameItem(item.index.toString(), name)}
onMissingItems={(itemIds) => console.log('missing', itemIds)}
// onMissingItems={(itemIds) => console.log('missing', itemIds)}
onDrop={(item, target) => console.log('drop', item, target)}
onExpandItem={(item) => console.log('expand', item)}
viewState={{}}>
onExpandItem={(item) => { console.log('expand', item); FileTreeState.refresh() }}
viewState={{
'filetree': {},
}}
>
<Tree treeId="filetree" treeLabel="Explorer" rootItem="root"/>
</UncontrolledTreeEnvironment>
</div>
Expand All @@ -64,50 +79,35 @@ export function FileTree(props: FileTreeProps) {
class TreeProvider<T = any> implements RCT.TreeDataProvider {
private data: RCT.ExplicitDataSource;
private onDidChangeTreeDataEmitter = new EventEmitter<RCT.TreeItemIndex[]>();
private setItemName?: (item: RCT.TreeItem<T>, newName: string) => RCT.TreeItem<T>;
// private setItemName?: (item: RCT.TreeItem<T>, newName: string) => RCT.TreeItem<T>;

constructor(
items: Record<RCT.TreeItemIndex, RCT.TreeItem<T>>,
setItemName?: (item: RCT.TreeItem<T>, newName: string) => RCT.TreeItem<T>,
// setItemName?: (item: RCT.TreeItem<T>, newName: string) => RCT.TreeItem<T>,
) {
debug('TreeProvider constructor', items);
this.data = {items};
this.setItemName = setItemName;
// this.setItemName = setItemName;
}

public async updateItems(items: Record<RCT.TreeItemIndex, RCT.TreeItem<T>>) {
// const changed: Partial<Record<RCT.TreeItemIndex, RCT.TreeItem<T>>> = diff(this.data.items, items);
// console.log(changed);

debug('updateItems items', items)
this.data = {items};
this.onChangeItemChildren('root', Object.keys(this.data.items).filter(i => i !== 'root'));

// update sub children
/*for (const key of Object.keys(changed)) {
const children = this.data.items[key]?.children;
if (key && children) {
this.onChangeItemChildren(key, children);
}
}*/
this.onDidChangeTreeDataEmitter.emit(['root']);
}

public async getTreeItem(itemId: RCT.TreeItemIndex): Promise<RCT.TreeItem> {
debug('getTreeItem', itemId, this.data.items[itemId]);
return this.data.items[itemId];
}

public async onChangeItemChildren(itemId: RCT.TreeItemIndex, newChildren: RCT.TreeItemIndex[]): Promise<void> {
this.data.items[itemId].children = newChildren;
this.onDidChangeTreeDataEmitter.emit([itemId]);
}


public onDidChangeTreeData(listener: (changedItemIds: RCT.TreeItemIndex[]) => void): RCT.Disposable {
debug('onDidChangeTreeData items', this.data.items);
const handlerId = this.onDidChangeTreeDataEmitter.on(payload => listener(payload));
return {dispose: () => this.onDidChangeTreeDataEmitter.off(handlerId)};
}

public async onRenameItem(item: RCT.TreeItem<any>, name: string): Promise<void> {
if (this.setItemName) {
this.data.items[item.index] = this.setItemName(item, name);
this.onDidChangeTreeDataEmitter.emit([item.index]);
}
}
// public async onChangeItemChildren(itemId: RCT.TreeItemIndex, newChildren: RCT.TreeItemIndex[]): Promise<void> {
// public async onRenameItem(item: RCT.TreeItem<any>, name: string): Promise<void> {
}
26 changes: 21 additions & 5 deletions src/hooks/useShell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import {Terminal} from 'xterm';
import {FitAddon} from 'xterm-addon-fit';
import {startFiles} from '../utils/webcontainer';
import {useDarkMode} from '../hooks/useDarkMode';
import {FileTreeState} from '../components/FileTree'
import Debug from '../utils/debug';

const debug = Debug('useShell');

import type {WebContainerProcess} from '@webcontainer/api';
import type {GridviewPanelApi} from 'dockview';
Expand Down Expand Up @@ -35,7 +39,7 @@ export function useShell(): ShellInstance {

const start = useCallback(async (root: HTMLElement, panel: GridviewPanelApi, onServerReady?: ServerReadyHandler) => {
if (container) return;
console.log('Booting...');
debug('Booting...');
const shell = await WebContainer.boot({workdirName: 'vslite'});
const terminal = new Terminal({convertEol: true, theme});
const addon = new FitAddon();
Expand All @@ -46,13 +50,24 @@ export function useShell(): ShellInstance {
let watchReady = false;
const watch = await shell.spawn('npx', ['-y', 'chokidar-cli', '.', '-i', '"(**/(node_modules|.git|_tmp_)**)"']);
watch.output.pipeTo(new WritableStream({
write(data) {
async write(data) {
const type: string = data.split(':').at(0) || ''
if (watchReady) {
console.log('Change detected: ', data);
debug('Change detected: ', data);
} else if (data.includes('Watching "."')) {
console.log('File watcher ready.');
debug('File watcher ready.');
watchReady = true;
}
switch (type) {
case 'change':
break;
case 'add':
case 'unlink':
case 'addDir':
case 'unlinkDir':
default:
FileTreeState.refresh(data);
}
}
}));
// Start shell
Expand All @@ -62,6 +77,7 @@ export function useShell(): ShellInstance {
const input = jsh.input.getWriter();
await init.read();
await input.write(`alias git='npx -y g4c@stable'\n\f`);
await input.write(`alias vslite-clone='git clone github.com/kat-tax/vslite'\n\f`)
init.releaseLock();
// Pipe terminal to shell and vice versa
terminal.onData(data => {input.write(data)});
Expand All @@ -83,7 +99,7 @@ export function useShell(): ShellInstance {
shell.on('server-ready', (port, url) => onServerReady && onServerReady(url, port));
setContainer(shell);
setTerminal(terminal);
console.log('Done.');
debug('Done.');
}, []);

return {terminal, container, process, start};
Expand Down
4 changes: 4 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ import {Dock} from './components/Dock';

const el = document.getElementById('root');
el && createRoot(el).render(<Dock/>);

if (import.meta.env.DEV && !globalThis.localStorage?.debug) {
console.log('To enable debug logging, use', '\`localStorage.debug = "vslite"\`')
}
9 changes: 9 additions & 0 deletions src/utils/debounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// https://gist.github.com/ca0v/73a31f57b397606c9813472f7493a940
export function debounce<T extends Function>(cb: T, wait = 150) {
let h: NodeJS.Timeout;
let callable = (...args: any) => {
clearTimeout(h);
h = setTimeout(() => cb(...args), wait);
};
return <T>(<any>callable);
}
19 changes: 19 additions & 0 deletions src/utils/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Isomorphic minimal debug package compatible with npm:debug
// TODO: Vite uses picomatch... we could borrow it
export const NS = 'vslite'
const CONFIG = globalThis.localStorage?.debug ?
globalThis.localStorage?.debug :
globalThis.process?.env.DEBUG || ''
const isEnabled = CONFIG.split(',').find((m: string) => m.startsWith(NS) || m === '*')

const Debug = (name: string) => {
const prefix = `[${NS}/${name}]`
const debug = (...all: any) => {
if (isEnabled) {
console.debug(prefix, ...all)
}
}
return debug
}

export default Debug
12 changes: 11 additions & 1 deletion src/utils/webcontainer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type {FileSystemAPI, FileSystemTree} from '@webcontainer/api';
import type {TreeItem, TreeItemIndex} from 'react-complex-tree';
import Debug from '../utils/debug';

const debug = Debug('webcontainer')
const configRaw = globalThis.localStorage?.vslite_config
const config = configRaw ? JSON.parse(configRaw) : {}

export async function getDirAsTree(
fs: FileSystemAPI,
Expand All @@ -8,7 +13,11 @@ export async function getDirAsTree(
root: TreeItem<string>,
db: Record<TreeItemIndex, TreeItem<string>>,
) {
const dir = await fs.readdir(path, {withFileTypes: true});
const dirAll = await fs.readdir(path, {withFileTypes: true});
const dir = config.showHidden ? dirAll : dirAll.filter((item) =>
!item.name.startsWith('.') && item.name !== 'node_modules'
);
debug('getDirAsTree() dir', dir)
if (parent === 'root') db.root = root;
dir.forEach(item => {
const isDir = item.isDirectory();
Expand All @@ -24,6 +33,7 @@ export async function getDirAsTree(
if (parent) db?.[parent]?.children?.push(itemPath);
if (isDir) return getDirAsTree(fs, itemPath, itemPath, root, db);
});
debug('getDirAsTree() db', db)
return db;
}

Expand Down
Loading

0 comments on commit 2a34a9c

Please sign in to comment.