mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-31 05:32:58 +00:00
* fix(app): open folders from inline path links in the file explorer Clicking a folder path in an assistant message opened a file tab that failed with "Requested path is not a file". Extension-less workspace-relative paths without a line number are now probed with a directory listing before opening: directories open the Files explorer navigated to that folder, everything else opens as a file like before. Lexical directories (trailing slash, workspace root) now navigate the explorer to the folder instead of only preloading its listing. * fix(app): reveal linked folders and their contents Resolve workspace folder links from directory metadata and expand their ancestor paths in Files. Preserve file and line-reference opening, including extensionless files. Co-authored-by: Moy <moyfoxther@gmail.com> * fix(app): cancel stale path opens and reveal hidden folders Keep pending directory checks inside the path-opening model so a later click or navigation supersedes them. Explicitly revealing a hidden directory also enables the existing hidden-file visibility preference. --------- Co-authored-by: Moy <moyfoxther@gmail.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
225 lines
6.4 KiB
TypeScript
225 lines
6.4 KiB
TypeScript
import type { ExplorerDirectory, ExplorerEntry } from "@/stores/session-store";
|
|
import type { SortOption } from "@/stores/panel-store/state";
|
|
import { filterVisibleExplorerEntries, isHiddenExplorerPath } from "./visibility";
|
|
|
|
export const MAX_AUTO_EXPANDED_DIRECTORY_DEPTH = 5;
|
|
|
|
export interface ExplorerTreeRow {
|
|
entry: ExplorerEntry;
|
|
depth: number;
|
|
}
|
|
|
|
interface FlattenExplorerTreeInput {
|
|
directories: ReadonlyMap<string, ExplorerDirectory>;
|
|
expandedPaths: ReadonlySet<string>;
|
|
sortOption: SortOption;
|
|
showHiddenFiles: boolean;
|
|
}
|
|
|
|
interface RestoreExpandedDirectoriesInput {
|
|
rootDirectory: ExplorerDirectory;
|
|
persistedExpandedPaths: ReadonlySet<string>;
|
|
showHiddenFiles: boolean;
|
|
requestDirectoryListing: (path: string) => Promise<ExplorerDirectory | null>;
|
|
}
|
|
|
|
interface ShowHiddenFilesAndRestoreExpandedDirectoriesInput extends Omit<
|
|
RestoreExpandedDirectoriesInput,
|
|
"showHiddenFiles"
|
|
> {
|
|
showHiddenFiles: () => void;
|
|
}
|
|
|
|
interface ReconcileRestoredExpandedPathsInput {
|
|
persistedExpandedPaths: ReadonlySet<string>;
|
|
currentExpandedPaths: ReadonlySet<string>;
|
|
restoredExpandedPaths: string[];
|
|
}
|
|
|
|
interface SetExpandedDirectoryPathInput {
|
|
currentExpandedPaths: readonly string[];
|
|
directoryPath: string;
|
|
expanded: boolean;
|
|
}
|
|
|
|
export function flattenExplorerTree({
|
|
directories,
|
|
expandedPaths,
|
|
sortOption,
|
|
showHiddenFiles,
|
|
}: FlattenExplorerTreeInput): ExplorerTreeRow[] {
|
|
const root = directories.get(".");
|
|
if (!root) {
|
|
return [];
|
|
}
|
|
|
|
const rows: ExplorerTreeRow[] = [];
|
|
const pending = rowsForDirectory(root, 0, sortOption, showHiddenFiles).toReversed();
|
|
|
|
while (pending.length > 0) {
|
|
const row = pending.pop();
|
|
if (!row) {
|
|
break;
|
|
}
|
|
rows.push(row);
|
|
|
|
const entry = row.entry;
|
|
if (entry.kind !== "directory" || !expandedPaths.has(entry.path)) {
|
|
continue;
|
|
}
|
|
const childDirectory = directories.get(entry.path);
|
|
if (!childDirectory) {
|
|
continue;
|
|
}
|
|
const childRows = rowsForDirectory(childDirectory, row.depth + 1, sortOption, showHiddenFiles);
|
|
for (let index = childRows.length - 1; index >= 0; index -= 1) {
|
|
pending.push(childRows[index]);
|
|
}
|
|
}
|
|
|
|
return rows;
|
|
}
|
|
|
|
export async function restoreExpandedDirectories({
|
|
rootDirectory,
|
|
persistedExpandedPaths,
|
|
showHiddenFiles,
|
|
requestDirectoryListing,
|
|
}: RestoreExpandedDirectoriesInput): Promise<string[]> {
|
|
const restoredPaths = ["."];
|
|
const restoredPathSet = new Set(restoredPaths);
|
|
let parentDirectories = [rootDirectory];
|
|
|
|
for (let depth = 1; depth <= MAX_AUTO_EXPANDED_DIRECTORY_DEPTH; depth += 1) {
|
|
const pathsToRequest: string[] = [];
|
|
for (const directory of parentDirectories) {
|
|
const entries = filterVisibleExplorerEntries(directory.entries, showHiddenFiles);
|
|
for (const entry of entries) {
|
|
const isPersistedExpandedDirectory =
|
|
entry.kind === "directory" && persistedExpandedPaths.has(entry.path);
|
|
if (isPersistedExpandedDirectory && !restoredPathSet.has(entry.path)) {
|
|
pathsToRequest.push(entry.path);
|
|
restoredPathSet.add(entry.path);
|
|
}
|
|
}
|
|
}
|
|
if (pathsToRequest.length === 0) {
|
|
break;
|
|
}
|
|
|
|
const requestedDirectories = await Promise.all(
|
|
pathsToRequest.map((path) => requestDirectoryListing(path)),
|
|
);
|
|
parentDirectories = [];
|
|
for (const directory of requestedDirectories) {
|
|
if (!directory) {
|
|
continue;
|
|
}
|
|
restoredPaths.push(directory.path);
|
|
parentDirectories.push(directory);
|
|
}
|
|
}
|
|
|
|
return restoredPaths;
|
|
}
|
|
|
|
export function showHiddenFilesAndRestoreExpandedDirectories({
|
|
rootDirectory,
|
|
persistedExpandedPaths,
|
|
showHiddenFiles,
|
|
requestDirectoryListing,
|
|
}: ShowHiddenFilesAndRestoreExpandedDirectoriesInput): Promise<string[]> {
|
|
showHiddenFiles();
|
|
return restoreExpandedDirectories({
|
|
rootDirectory,
|
|
persistedExpandedPaths,
|
|
showHiddenFiles: true,
|
|
requestDirectoryListing,
|
|
});
|
|
}
|
|
|
|
export function reconcileRestoredExpandedPaths({
|
|
persistedExpandedPaths,
|
|
currentExpandedPaths,
|
|
restoredExpandedPaths,
|
|
}: ReconcileRestoredExpandedPathsInput): string[] {
|
|
const reconciledPaths = new Set(restoredExpandedPaths);
|
|
|
|
for (const path of persistedExpandedPaths) {
|
|
if (!currentExpandedPaths.has(path)) {
|
|
reconciledPaths.delete(path);
|
|
}
|
|
}
|
|
for (const path of currentExpandedPaths) {
|
|
if (!persistedExpandedPaths.has(path)) {
|
|
reconciledPaths.add(path);
|
|
}
|
|
}
|
|
|
|
return Array.from(reconciledPaths);
|
|
}
|
|
|
|
export function setExpandedDirectoryPath({
|
|
currentExpandedPaths,
|
|
directoryPath,
|
|
expanded,
|
|
}: SetExpandedDirectoryPathInput): string[] {
|
|
const nextPaths = new Set(currentExpandedPaths);
|
|
if (expanded) {
|
|
nextPaths.add(directoryPath);
|
|
} else {
|
|
nextPaths.delete(directoryPath);
|
|
}
|
|
return Array.from(nextPaths);
|
|
}
|
|
|
|
function rowsForDirectory(
|
|
directory: ExplorerDirectory,
|
|
depth: number,
|
|
sortOption: SortOption,
|
|
showHiddenFiles: boolean,
|
|
): ExplorerTreeRow[] {
|
|
const visibleEntries = filterVisibleExplorerEntries(directory.entries, showHiddenFiles);
|
|
const sortedEntries = sortExplorerEntries(visibleEntries, sortOption);
|
|
return sortedEntries.map((entry) => ({ entry, depth }));
|
|
}
|
|
|
|
function sortExplorerEntries(entries: ExplorerEntry[], sortOption: SortOption): ExplorerEntry[] {
|
|
const sorted = [...entries];
|
|
sorted.sort((a, b) => {
|
|
if (a.kind !== b.kind) {
|
|
return a.kind === "directory" ? -1 : 1;
|
|
}
|
|
switch (sortOption) {
|
|
case "name":
|
|
return a.name.localeCompare(b.name);
|
|
case "modified":
|
|
return new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime();
|
|
case "size":
|
|
return b.size - a.size;
|
|
}
|
|
});
|
|
return sorted;
|
|
}
|
|
|
|
/** Load and expand the path to a directory selected outside the tree. */
|
|
export async function revealExplorerDirectory(input: {
|
|
directoryPath: string;
|
|
requestDirectoryListing: (path: string) => Promise<ExplorerDirectory | null>;
|
|
expandPaths: (paths: string[]) => void;
|
|
showHiddenFiles: () => void;
|
|
}): Promise<void> {
|
|
if (isHiddenExplorerPath(input.directoryPath)) input.showHiddenFiles();
|
|
const paths = ["."];
|
|
if (input.directoryPath !== ".") {
|
|
const segments = input.directoryPath.split("/").filter(Boolean);
|
|
for (let length = 1; length <= segments.length; length += 1) {
|
|
paths.push(segments.slice(0, length).join("/"));
|
|
}
|
|
}
|
|
for (const path of paths) {
|
|
if (!(await input.requestDirectoryListing(path))) return;
|
|
}
|
|
input.expandPaths(paths);
|
|
}
|