edit
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
<script lang="ts">
|
||||
import { ChevronDown, ChevronRight, FileText, Folder, FolderOpen } from "@lucide/svelte";
|
||||
import type { ExplorerNode, ExplorerNodeKind, FileStatusKind, GitRepositoryFile } from "../types";
|
||||
|
||||
interface Props {
|
||||
repoFiles: GitRepositoryFile[];
|
||||
expandedExplorerPaths: Set<string>;
|
||||
selectedExplorerPath: string;
|
||||
selectedExplorerKind: ExplorerNodeKind;
|
||||
hasRepository: boolean;
|
||||
isBusy: boolean;
|
||||
onToggleFolder: (node: ExplorerNode) => void;
|
||||
onSelectNode: (node: ExplorerNode) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
repoFiles = [],
|
||||
expandedExplorerPaths = new Set(),
|
||||
selectedExplorerPath = "",
|
||||
selectedExplorerKind = "file",
|
||||
hasRepository = false,
|
||||
isBusy = false,
|
||||
onToggleFolder = () => {},
|
||||
onSelectNode = () => {},
|
||||
}: Props = $props();
|
||||
|
||||
function mergeExplorerStatus(current: FileStatusKind | null, next: FileStatusKind | null): FileStatusKind | null {
|
||||
if (!next) return current;
|
||||
if (!current) return next;
|
||||
const priority: FileStatusKind[] = ["conflicted", "modified", "renamed", "deleted", "added", "untracked", "unknown"];
|
||||
return priority.indexOf(next) < priority.indexOf(current) ? next : current;
|
||||
}
|
||||
|
||||
function sortExplorerNodes(nodes: ExplorerNode[]) {
|
||||
nodes.sort((a, b) => {
|
||||
if (a.kind !== b.kind) return a.kind === "folder" ? -1 : 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
for (const node of nodes) sortExplorerNodes(node.children);
|
||||
}
|
||||
|
||||
function buildExplorerTree(files: GitRepositoryFile[]): ExplorerNode[] {
|
||||
const roots: ExplorerNode[] = [];
|
||||
const folders = new Map<string, ExplorerNode>();
|
||||
|
||||
for (const file of [...files].sort((a, b) => a.path.localeCompare(b.path))) {
|
||||
const parts = file.path.split(/[\\/]+/).filter(Boolean);
|
||||
let currentPath = "";
|
||||
let siblings = roots;
|
||||
let ancestors: ExplorerNode[] = [];
|
||||
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i];
|
||||
let folderNode = folders.get(currentPath);
|
||||
if (!folderNode) {
|
||||
folderNode = { name: parts[i], path: currentPath, kind: "folder", status: null, tracked: true, depth: i, children: [] };
|
||||
folders.set(currentPath, folderNode);
|
||||
siblings.push(folderNode);
|
||||
}
|
||||
ancestors = [...ancestors, folderNode];
|
||||
siblings = folderNode.children;
|
||||
}
|
||||
|
||||
const fileNode: ExplorerNode = {
|
||||
name: parts[parts.length - 1] ?? file.path,
|
||||
path: file.path,
|
||||
kind: "file",
|
||||
status: file.status,
|
||||
tracked: file.tracked,
|
||||
depth: Math.max(parts.length - 1, 0),
|
||||
children: [],
|
||||
};
|
||||
siblings.push(fileNode);
|
||||
|
||||
for (const folderNode of ancestors) {
|
||||
folderNode.status = mergeExplorerStatus(folderNode.status, file.status);
|
||||
folderNode.tracked = folderNode.tracked && file.tracked;
|
||||
}
|
||||
}
|
||||
|
||||
sortExplorerNodes(roots);
|
||||
return roots;
|
||||
}
|
||||
|
||||
function flattenExplorerTree(nodes: ExplorerNode[], expanded: Set<string>): ExplorerNode[] {
|
||||
const visible: ExplorerNode[] = [];
|
||||
for (const node of nodes) {
|
||||
visible.push(node);
|
||||
if (node.kind === "folder" && expanded.has(node.path)) {
|
||||
visible.push(...flattenExplorerTree(node.children, expanded));
|
||||
}
|
||||
}
|
||||
return visible;
|
||||
}
|
||||
|
||||
function statusLabel(kind: FileStatusKind | null): string {
|
||||
return kind ?? "none";
|
||||
}
|
||||
|
||||
let explorerTree = $derived(buildExplorerTree(repoFiles));
|
||||
let visibleNodes = $derived(flattenExplorerTree(explorerTree, expandedExplorerPaths));
|
||||
</script>
|
||||
|
||||
<section class="panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="File explorer">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<span class="eyebrow">Explorer</span>
|
||||
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Files</h2>
|
||||
</div>
|
||||
<span class="pill pill-count">{repoFiles.length}</span>
|
||||
</div>
|
||||
|
||||
{#if !hasRepository}
|
||||
<p class="blank-state">Open a repository to browse files.</p>
|
||||
{:else if repoFiles.length === 0}
|
||||
<p class="blank-state">No files returned.</p>
|
||||
{:else}
|
||||
<div class="explorer-list overflow-auto p-2">
|
||||
{#each visibleNodes as node (`${node.kind}:${node.path}`)}
|
||||
<div
|
||||
class="explorer-row"
|
||||
class:active={selectedExplorerPath === node.path && selectedExplorerKind === node.kind}
|
||||
class:folder={node.kind === "folder"}
|
||||
style={`--depth: ${node.depth}`}
|
||||
title={node.path}
|
||||
>
|
||||
{#if node.kind === "folder"}
|
||||
<button
|
||||
class="tree-toggle"
|
||||
type="button"
|
||||
onclick={() => onToggleFolder(node)}
|
||||
disabled={isBusy}
|
||||
title={expandedExplorerPaths.has(node.path) ? "Collapse folder" : "Expand folder"}
|
||||
>
|
||||
{#if expandedExplorerPaths.has(node.path)}
|
||||
<ChevronDown size={14} aria-hidden="true" />
|
||||
{:else}
|
||||
<ChevronRight size={14} aria-hidden="true" />
|
||||
{/if}
|
||||
</button>
|
||||
{#if expandedExplorerPaths.has(node.path)}
|
||||
<FolderOpen size={15} aria-hidden="true" />
|
||||
{:else}
|
||||
<Folder size={15} aria-hidden="true" />
|
||||
{/if}
|
||||
{:else}
|
||||
<span class="tree-spacer"></span>
|
||||
<FileText size={15} aria-hidden="true" />
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="explorer-select"
|
||||
type="button"
|
||||
onclick={() => onSelectNode(node)}
|
||||
disabled={isBusy}
|
||||
title={`Show history for ${node.path}`}
|
||||
>
|
||||
<span>{node.name}</span>
|
||||
</button>
|
||||
|
||||
{#if node.status}
|
||||
<small class={`status-badge ${node.status}`}>{statusLabel(node.status)}</small>
|
||||
{:else if !node.tracked}
|
||||
<small class="status-badge untracked">untracked</small>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
Reference in New Issue
Block a user