@lvce-editor/explorer-view 6.1.0 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/explorerViewWorkerMain.js +891 -832
- package/package.json +1 -1
|
@@ -3500,1023 +3500,1075 @@ const handleDragStart = state => {
|
|
|
3500
3500
|
return state;
|
|
3501
3501
|
};
|
|
3502
3502
|
|
|
3503
|
-
const getChildHandles = async fileHandle => {
|
|
3504
|
-
// @ts-ignore
|
|
3505
|
-
const values = fileHandle.values();
|
|
3506
|
-
const children = await fromAsync(values);
|
|
3507
|
-
return children;
|
|
3508
|
-
};
|
|
3509
|
-
|
|
3510
|
-
const getFileHandleText = async fileHandle => {
|
|
3511
|
-
const file = await fileHandle.getFile();
|
|
3512
|
-
const text = await file.text();
|
|
3513
|
-
return text;
|
|
3514
|
-
};
|
|
3515
|
-
|
|
3516
3503
|
const isDirectoryHandle = fileHandle => {
|
|
3517
3504
|
return fileHandle.kind === 'directory';
|
|
3518
3505
|
};
|
|
3519
3506
|
|
|
3520
|
-
const
|
|
3521
|
-
return
|
|
3507
|
+
const isValid = decoration => {
|
|
3508
|
+
return decoration && typeof decoration.decoration === 'string' && typeof decoration.uri === 'string';
|
|
3522
3509
|
};
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
const normalized = fileHandles.filter(Boolean);
|
|
3527
|
-
for (const fileHandle of normalized) {
|
|
3528
|
-
const {
|
|
3529
|
-
name
|
|
3530
|
-
} = fileHandle;
|
|
3531
|
-
if (isDirectoryHandle(fileHandle)) {
|
|
3532
|
-
const children = await getChildHandles(fileHandle);
|
|
3533
|
-
const childTree = await createUploadTree(name, children);
|
|
3534
|
-
uploadTree[name] = childTree;
|
|
3535
|
-
} else if (isFileHandle(fileHandle)) {
|
|
3536
|
-
// TODO maybe save blob and use filesystem.writeblob
|
|
3537
|
-
const text = await getFileHandleText(fileHandle);
|
|
3538
|
-
uploadTree[name] = text;
|
|
3539
|
-
}
|
|
3510
|
+
const normalizeDecorations = decorations => {
|
|
3511
|
+
if (!decorations || !Array.isArray(decorations)) {
|
|
3512
|
+
return [];
|
|
3540
3513
|
}
|
|
3541
|
-
return
|
|
3514
|
+
return decorations.filter(isValid);
|
|
3542
3515
|
};
|
|
3543
3516
|
|
|
3544
|
-
const
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
const fullPath = currentPath ? join2(currentPath, path) : path;
|
|
3549
|
-
if (typeof value === 'object') {
|
|
3550
|
-
operations.push({
|
|
3551
|
-
path: join2(root, fullPath),
|
|
3552
|
-
type: CreateFolder$1
|
|
3553
|
-
});
|
|
3554
|
-
processTree(value, fullPath);
|
|
3555
|
-
} else if (typeof value === 'string') {
|
|
3556
|
-
operations.push({
|
|
3557
|
-
path: join2(root, fullPath),
|
|
3558
|
-
text: value,
|
|
3559
|
-
type: CreateFile$1
|
|
3560
|
-
});
|
|
3561
|
-
}
|
|
3517
|
+
const getFileDecorations = async (scheme, root, maybeUris, decorationsEnabled, assetDir, platform) => {
|
|
3518
|
+
try {
|
|
3519
|
+
if (!decorationsEnabled) {
|
|
3520
|
+
return [];
|
|
3562
3521
|
}
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3522
|
+
const providerIds = await invoke$1('SourceControl.getEnabledProviderIds', scheme, root, assetDir, platform);
|
|
3523
|
+
if (providerIds.length === 0) {
|
|
3524
|
+
return [];
|
|
3525
|
+
}
|
|
3526
|
+
// TODO how to handle multiple providers?
|
|
3527
|
+
const providerId = providerIds.at(-1);
|
|
3528
|
+
const uris = ensureUris(maybeUris);
|
|
3529
|
+
const decorations = await invoke$1('SourceControl.getFileDecorations', providerId, uris, assetDir, platform);
|
|
3530
|
+
const normalized = normalizeDecorations(decorations);
|
|
3531
|
+
return normalized;
|
|
3532
|
+
} catch (error) {
|
|
3533
|
+
console.error(error);
|
|
3534
|
+
return [];
|
|
3535
|
+
}
|
|
3566
3536
|
};
|
|
3567
3537
|
|
|
3568
|
-
const
|
|
3569
|
-
|
|
3570
|
-
|
|
3538
|
+
const RE_PROTOCOL = /^[a-z+]:\/\//;
|
|
3539
|
+
const getScheme = uri => {
|
|
3540
|
+
const match = uri.match(RE_PROTOCOL);
|
|
3541
|
+
if (!match) {
|
|
3542
|
+
return '';
|
|
3571
3543
|
}
|
|
3572
|
-
|
|
3573
|
-
const fileOperations = getFileOperations(root, uploadTree);
|
|
3574
|
-
await applyFileOperations(fileOperations);
|
|
3575
|
-
|
|
3576
|
-
// TODO
|
|
3577
|
-
// 1. in electron, use webutils.getPathForFile to see if a path is available
|
|
3578
|
-
// 2. else, walk all files and folders recursively and upload all of them (if there are many, show a progress bar)
|
|
3579
|
-
|
|
3580
|
-
// TODO send file system operations to renderer worker
|
|
3581
|
-
return true;
|
|
3544
|
+
return match[0];
|
|
3582
3545
|
};
|
|
3583
3546
|
|
|
3584
|
-
const
|
|
3585
|
-
return
|
|
3586
|
-
|
|
3587
|
-
const
|
|
3588
|
-
const
|
|
3589
|
-
const
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
const
|
|
3593
|
-
const
|
|
3594
|
-
|
|
3595
|
-
pathSeparator,
|
|
3596
|
-
root
|
|
3597
|
-
} = state;
|
|
3598
|
-
const handled = await uploadFileSystemHandles(root, pathSeparator, fileHandles);
|
|
3599
|
-
if (handled) {
|
|
3600
|
-
const updated = await refresh(state);
|
|
3601
|
-
return {
|
|
3602
|
-
...updated,
|
|
3603
|
-
dropTargets: []
|
|
3604
|
-
};
|
|
3605
|
-
}
|
|
3606
|
-
const mergedDirents = await getMergedDirents$2(root, pathSeparator, items);
|
|
3547
|
+
const getSettings = async () => {
|
|
3548
|
+
// TODO don't return false always
|
|
3549
|
+
// TODO get all settings at once
|
|
3550
|
+
const useChevronsRaw = await invoke$2('Preferences.get', 'explorer.useChevrons');
|
|
3551
|
+
const useChevrons = useChevronsRaw === false ? false : true;
|
|
3552
|
+
const confirmDeleteRaw = await invoke$2('Preferences.get', 'explorer.confirmdelete');
|
|
3553
|
+
const confirmDelete = confirmDeleteRaw === false ? false : false;
|
|
3554
|
+
const confirmPasteRaw = await invoke$2('Preferences.get', 'explorer.confirmpaste');
|
|
3555
|
+
const confirmPaste = confirmPasteRaw === false ? false : false;
|
|
3556
|
+
const sourceControlDecorationsRaw = await invoke$2('Preferences.get', 'explorer.sourceControlDecorations');
|
|
3557
|
+
const sourceControlDecorations = sourceControlDecorationsRaw === false ? false : true;
|
|
3607
3558
|
return {
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3559
|
+
confirmDelete,
|
|
3560
|
+
confirmPaste,
|
|
3561
|
+
sourceControlDecorations,
|
|
3562
|
+
useChevrons
|
|
3611
3563
|
};
|
|
3612
3564
|
};
|
|
3613
3565
|
|
|
3614
|
-
const
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3566
|
+
const getWorkspacePath = () => {
|
|
3567
|
+
return invoke$2('Workspace.getPath');
|
|
3568
|
+
};
|
|
3569
|
+
|
|
3570
|
+
const getSavedChildDirents = (map, path, depth, excluded, pathSeparator) => {
|
|
3571
|
+
let children = map[path];
|
|
3572
|
+
if (!children) {
|
|
3573
|
+
return [];
|
|
3574
|
+
}
|
|
3575
|
+
const dirents = [];
|
|
3576
|
+
children = sortExplorerItems(children);
|
|
3577
|
+
const visible = [];
|
|
3578
|
+
const displayRoot = path.endsWith(pathSeparator) ? path : path + pathSeparator;
|
|
3579
|
+
for (const child of children) {
|
|
3580
|
+
if (excluded.includes(child.name)) {
|
|
3581
|
+
continue;
|
|
3582
|
+
}
|
|
3583
|
+
visible.push(child);
|
|
3584
|
+
}
|
|
3585
|
+
const visibleLength = visible.length;
|
|
3586
|
+
for (let i = 0; i < visibleLength; i++) {
|
|
3587
|
+
const child = visible[i];
|
|
3618
3588
|
const {
|
|
3619
|
-
name
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3589
|
+
name,
|
|
3590
|
+
type
|
|
3591
|
+
} = child;
|
|
3592
|
+
const childPath = displayRoot + name;
|
|
3593
|
+
if ((child.type === Directory || child.type === SymLinkFolder) && childPath in map) {
|
|
3594
|
+
dirents.push({
|
|
3595
|
+
depth,
|
|
3596
|
+
icon: '',
|
|
3597
|
+
name,
|
|
3598
|
+
path: childPath,
|
|
3599
|
+
posInSet: i + 1,
|
|
3600
|
+
setSize: visibleLength,
|
|
3601
|
+
type: DirectoryExpanded
|
|
3602
|
+
});
|
|
3603
|
+
dirents.push(...getSavedChildDirents(map, childPath, depth + 1, excluded, pathSeparator));
|
|
3604
|
+
} else {
|
|
3605
|
+
dirents.push({
|
|
3606
|
+
depth,
|
|
3607
|
+
icon: '',
|
|
3608
|
+
name,
|
|
3609
|
+
path: childPath,
|
|
3610
|
+
posInSet: i + 1,
|
|
3611
|
+
setSize: visibleLength,
|
|
3612
|
+
type
|
|
3613
|
+
});
|
|
3614
|
+
}
|
|
3627
3615
|
}
|
|
3628
|
-
return
|
|
3616
|
+
return dirents;
|
|
3629
3617
|
};
|
|
3630
3618
|
|
|
3631
|
-
|
|
3632
|
-
const
|
|
3633
|
-
const pathSeparator = await getPathSeparator$1(root);
|
|
3634
|
-
const operations = await getFileOperationsElectron(root, paths, fileHandles, pathSeparator);
|
|
3635
|
-
await applyFileOperations(operations);
|
|
3636
|
-
};
|
|
3619
|
+
const Fulfilled = 'fulfilled';
|
|
3620
|
+
const Rejected = 'rejected';
|
|
3637
3621
|
|
|
3638
|
-
const
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
root
|
|
3651
|
-
} = state;
|
|
3652
|
-
await copyFilesElectron(root, fileHandles, files, paths);
|
|
3653
|
-
const mergedDirents = await getMergedDirents$1(root, pathSeparator, items);
|
|
3654
|
-
return {
|
|
3655
|
-
...state,
|
|
3656
|
-
dropTargets: [],
|
|
3657
|
-
items: mergedDirents
|
|
3658
|
-
};
|
|
3622
|
+
const createDirents = (root, expandedDirentPaths, expandedDirentChildren, excluded, pathSeparator) => {
|
|
3623
|
+
const dirents = [];
|
|
3624
|
+
const map = Object.create(null);
|
|
3625
|
+
for (let i = 0; i < expandedDirentPaths.length; i++) {
|
|
3626
|
+
const path = expandedDirentPaths[i];
|
|
3627
|
+
const children = expandedDirentChildren[i];
|
|
3628
|
+
if (children.status === Fulfilled) {
|
|
3629
|
+
map[path] = children.value;
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
dirents.push(...getSavedChildDirents(map, root, 1, excluded, pathSeparator));
|
|
3633
|
+
return dirents;
|
|
3659
3634
|
};
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
const getModule = isElectron => {
|
|
3664
|
-
if (isElectron) {
|
|
3665
|
-
return handleDrop$1;
|
|
3635
|
+
const getSavedExpandedPaths = (savedState, root) => {
|
|
3636
|
+
if (savedState && savedState.root !== root) {
|
|
3637
|
+
return [];
|
|
3666
3638
|
}
|
|
3667
|
-
|
|
3639
|
+
if (savedState && savedState.expandedPaths && Array.isArray(savedState.expandedPaths)) {
|
|
3640
|
+
return savedState.expandedPaths;
|
|
3641
|
+
}
|
|
3642
|
+
return [];
|
|
3668
3643
|
};
|
|
3669
|
-
const
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3644
|
+
const restoreExpandedState = async (savedState, root, pathSeparator, excluded) => {
|
|
3645
|
+
// TODO read all opened folders in parallel
|
|
3646
|
+
// ignore ENOENT errors
|
|
3647
|
+
// ignore ENOTDIR errors
|
|
3648
|
+
// merge all dirents
|
|
3649
|
+
// restore scroll location
|
|
3650
|
+
const expandedPaths = getSavedExpandedPaths(savedState, root);
|
|
3651
|
+
if (root === EmptyString) {
|
|
3652
|
+
return [];
|
|
3653
|
+
}
|
|
3654
|
+
const expandedDirentPaths = [root, ...expandedPaths];
|
|
3655
|
+
const expandedDirentChildren = await Promise.allSettled(expandedDirentPaths.map(getChildDirentsRaw));
|
|
3656
|
+
if (expandedDirentChildren[0].status === Rejected) {
|
|
3657
|
+
throw expandedDirentChildren[0].reason;
|
|
3658
|
+
}
|
|
3659
|
+
const dirents = createDirents(root, expandedDirentPaths, expandedDirentChildren, excluded, pathSeparator);
|
|
3660
|
+
return dirents;
|
|
3673
3661
|
};
|
|
3674
3662
|
|
|
3675
|
-
const
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3663
|
+
const getPathSeparator = root => {
|
|
3664
|
+
return getPathSeparator$1(root);
|
|
3665
|
+
};
|
|
3666
|
+
const getExcluded = () => {
|
|
3667
|
+
const excludedObject = {};
|
|
3668
|
+
const excluded = [];
|
|
3669
|
+
for (const [key, value] of Object.entries(excludedObject)) {
|
|
3670
|
+
if (value) {
|
|
3671
|
+
excluded.push(key);
|
|
3679
3672
|
}
|
|
3680
3673
|
}
|
|
3681
|
-
return
|
|
3674
|
+
return excluded;
|
|
3682
3675
|
};
|
|
3683
|
-
const
|
|
3684
|
-
|
|
3685
|
-
const endIndex = getEndIndex(items, index, dirent);
|
|
3686
|
-
const mergedDirents = [...items.slice(0, startIndex), {
|
|
3687
|
-
...dirent,
|
|
3688
|
-
type: DirectoryExpanded
|
|
3689
|
-
}, ...childDirents, ...items.slice(endIndex)];
|
|
3690
|
-
return mergedDirents;
|
|
3676
|
+
const getSavedRoot = (savedState, workspacePath) => {
|
|
3677
|
+
return workspacePath;
|
|
3691
3678
|
};
|
|
3692
|
-
const
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
await uploadFileSystemHandles(dirent.path, '/', fileHandles);
|
|
3698
|
-
const childDirents = await getChildDirents(pathSeparator, dirent.path, dirent.depth);
|
|
3699
|
-
const mergedDirents = getMergedDirents(items, index, dirent, childDirents);
|
|
3700
|
-
// TODO update maxlineY
|
|
3701
|
-
return {
|
|
3702
|
-
...state,
|
|
3703
|
-
dropTargets: [],
|
|
3704
|
-
items: mergedDirents
|
|
3705
|
-
};
|
|
3679
|
+
const getErrorCode = error => {
|
|
3680
|
+
if (error && typeof error === 'object' && 'code' in error && typeof error.code === 'string') {
|
|
3681
|
+
return error.code;
|
|
3682
|
+
}
|
|
3683
|
+
return '';
|
|
3706
3684
|
};
|
|
3707
|
-
const
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
} = state;
|
|
3711
|
-
const parentIndex = getParentStartIndex(items, index);
|
|
3712
|
-
if (parentIndex === -1) {
|
|
3713
|
-
return handleDropRoot(state, fileHandles, files, paths);
|
|
3685
|
+
const getErrorMessage = error => {
|
|
3686
|
+
if (error instanceof Error) {
|
|
3687
|
+
return error.message;
|
|
3714
3688
|
}
|
|
3715
|
-
|
|
3689
|
+
if (typeof error === 'string') {
|
|
3690
|
+
return error;
|
|
3691
|
+
}
|
|
3692
|
+
return 'Unknown error';
|
|
3716
3693
|
};
|
|
3717
|
-
const
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
case
|
|
3727
|
-
|
|
3728
|
-
return handleDropIntoFolder(state, dirent, index, fileHandles);
|
|
3729
|
-
case File:
|
|
3730
|
-
return handleDropIntoFile(state, dirent, index, fileHandles, files, paths);
|
|
3694
|
+
const getFriendlyErrorMessage = (errorMessage, errorCode) => {
|
|
3695
|
+
switch (errorCode) {
|
|
3696
|
+
case 'EACCES':
|
|
3697
|
+
case 'EPERM':
|
|
3698
|
+
return 'permission was denied';
|
|
3699
|
+
case 'EBUSY':
|
|
3700
|
+
return 'the folder is currently in use';
|
|
3701
|
+
case 'ENOENT':
|
|
3702
|
+
return 'the folder does not exist';
|
|
3703
|
+
case 'ENOTDIR':
|
|
3704
|
+
return 'the path is not a folder';
|
|
3731
3705
|
default:
|
|
3732
|
-
return
|
|
3706
|
+
return errorMessage || 'an unexpected error occurred';
|
|
3733
3707
|
}
|
|
3734
3708
|
};
|
|
3735
|
-
|
|
3736
|
-
const
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3709
|
+
const loadContent = async (state, savedState) => {
|
|
3710
|
+
const {
|
|
3711
|
+
assetDir,
|
|
3712
|
+
platform
|
|
3713
|
+
} = state;
|
|
3714
|
+
const {
|
|
3715
|
+
confirmDelete,
|
|
3716
|
+
sourceControlDecorations,
|
|
3717
|
+
useChevrons
|
|
3718
|
+
} = await getSettings();
|
|
3719
|
+
const workspacePath = await getWorkspacePath();
|
|
3720
|
+
const root = getSavedRoot(savedState, workspacePath);
|
|
3721
|
+
try {
|
|
3722
|
+
// TODO path separator could be restored from saved state
|
|
3723
|
+
const pathSeparator = await getPathSeparator(root); // TODO only load path separator once
|
|
3724
|
+
const excluded = getExcluded();
|
|
3725
|
+
const restoredDirents = await restoreExpandedState(savedState, root, pathSeparator, excluded);
|
|
3726
|
+
let minLineY = 0;
|
|
3727
|
+
if (savedState && typeof savedState.minLineY === 'number') {
|
|
3728
|
+
minLineY = savedState.minLineY;
|
|
3729
|
+
}
|
|
3730
|
+
let deltaY = 0;
|
|
3731
|
+
if (savedState && typeof savedState.deltaY === 'number') {
|
|
3732
|
+
deltaY = savedState.deltaY;
|
|
3733
|
+
}
|
|
3734
|
+
const scheme = getScheme(root);
|
|
3735
|
+
const decorations = await getFileDecorations(scheme, root, restoredDirents.filter(item => item.depth === 1).map(item => item.path), sourceControlDecorations, assetDir, platform);
|
|
3736
|
+
return {
|
|
3737
|
+
...state,
|
|
3738
|
+
confirmDelete,
|
|
3739
|
+
decorations,
|
|
3740
|
+
deltaY,
|
|
3741
|
+
errorCode: '',
|
|
3742
|
+
errorMessage: '',
|
|
3743
|
+
excluded,
|
|
3744
|
+
hasError: false,
|
|
3745
|
+
initial: false,
|
|
3746
|
+
items: restoredDirents,
|
|
3747
|
+
maxIndent: 10,
|
|
3748
|
+
minLineY,
|
|
3749
|
+
pathSeparator,
|
|
3750
|
+
root,
|
|
3751
|
+
useChevrons
|
|
3752
|
+
};
|
|
3753
|
+
} catch (error) {
|
|
3754
|
+
const errorCode = getErrorCode(error);
|
|
3755
|
+
const errorMessage = getFriendlyErrorMessage(getErrorMessage(error), errorCode);
|
|
3756
|
+
return {
|
|
3757
|
+
...state,
|
|
3758
|
+
confirmDelete,
|
|
3759
|
+
errorCode,
|
|
3760
|
+
errorMessage,
|
|
3761
|
+
hasError: true,
|
|
3762
|
+
initial: false,
|
|
3763
|
+
items: [],
|
|
3764
|
+
root,
|
|
3765
|
+
useChevrons
|
|
3766
|
+
};
|
|
3742
3767
|
}
|
|
3743
3768
|
};
|
|
3744
3769
|
|
|
3745
|
-
const
|
|
3770
|
+
const getChildHandles = async fileHandle => {
|
|
3746
3771
|
// @ts-ignore
|
|
3747
|
-
const
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
const getFileHandles = async fileIds => {
|
|
3752
|
-
if (fileIds.length === 0) {
|
|
3753
|
-
return [];
|
|
3754
|
-
}
|
|
3755
|
-
const files = await getFileHandles$1(fileIds);
|
|
3756
|
-
return files;
|
|
3772
|
+
const values = fileHandle.values();
|
|
3773
|
+
const children = await fromAsync(values);
|
|
3774
|
+
return children;
|
|
3757
3775
|
};
|
|
3758
3776
|
|
|
3759
|
-
const
|
|
3760
|
-
|
|
3777
|
+
const getFileHandleText = async fileHandle => {
|
|
3778
|
+
const file = await fileHandle.getFile();
|
|
3779
|
+
const text = await file.text();
|
|
3780
|
+
return text;
|
|
3761
3781
|
};
|
|
3762
3782
|
|
|
3763
|
-
const
|
|
3764
|
-
return
|
|
3765
|
-
};
|
|
3766
|
-
const getFilePaths = async (files, platform) => {
|
|
3767
|
-
if (platform !== Electron) {
|
|
3768
|
-
return files.map(file => '');
|
|
3769
|
-
}
|
|
3770
|
-
const promises = files.map(getFilepath);
|
|
3771
|
-
const paths = await Promise.all(promises);
|
|
3772
|
-
return paths;
|
|
3783
|
+
const isFileHandle = fileHandle => {
|
|
3784
|
+
return fileHandle.kind === 'file';
|
|
3773
3785
|
};
|
|
3774
3786
|
|
|
3775
|
-
const
|
|
3776
|
-
|
|
3787
|
+
const createUploadTree = async (root, fileHandles) => {
|
|
3788
|
+
const uploadTree = Object.create(null);
|
|
3789
|
+
const normalized = fileHandles.filter(Boolean);
|
|
3790
|
+
for (const fileHandle of normalized) {
|
|
3777
3791
|
const {
|
|
3778
|
-
|
|
3779
|
-
} =
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3792
|
+
name
|
|
3793
|
+
} = fileHandle;
|
|
3794
|
+
if (isDirectoryHandle(fileHandle)) {
|
|
3795
|
+
const children = await getChildHandles(fileHandle);
|
|
3796
|
+
const childTree = await createUploadTree(name, children);
|
|
3797
|
+
uploadTree[name] = childTree;
|
|
3798
|
+
} else if (isFileHandle(fileHandle)) {
|
|
3799
|
+
// TODO maybe save blob and use filesystem.writeblob
|
|
3800
|
+
const text = await getFileHandleText(fileHandle);
|
|
3801
|
+
uploadTree[name] = text;
|
|
3802
|
+
}
|
|
3789
3803
|
}
|
|
3804
|
+
return uploadTree;
|
|
3790
3805
|
};
|
|
3791
3806
|
|
|
3792
|
-
const
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
} = state;
|
|
3812
|
-
const visible = items.slice(minLineY, maxLineY);
|
|
3813
|
-
const {
|
|
3814
|
-
icons,
|
|
3815
|
-
newFileIconCache
|
|
3816
|
-
} = await getFileIcons(visible, Object.create(null));
|
|
3817
|
-
return {
|
|
3818
|
-
...state,
|
|
3819
|
-
fileIconCache: newFileIconCache,
|
|
3820
|
-
icons
|
|
3807
|
+
const getFileOperations = (root, uploadTree) => {
|
|
3808
|
+
const operations = [];
|
|
3809
|
+
const processTree = (tree, currentPath) => {
|
|
3810
|
+
for (const [path, value] of Object.entries(tree)) {
|
|
3811
|
+
const fullPath = currentPath ? join2(currentPath, path) : path;
|
|
3812
|
+
if (typeof value === 'object') {
|
|
3813
|
+
operations.push({
|
|
3814
|
+
path: join2(root, fullPath),
|
|
3815
|
+
type: CreateFolder$1
|
|
3816
|
+
});
|
|
3817
|
+
processTree(value, fullPath);
|
|
3818
|
+
} else if (typeof value === 'string') {
|
|
3819
|
+
operations.push({
|
|
3820
|
+
path: join2(root, fullPath),
|
|
3821
|
+
text: value,
|
|
3822
|
+
type: CreateFile$1
|
|
3823
|
+
});
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3821
3826
|
};
|
|
3827
|
+
processTree(uploadTree, '');
|
|
3828
|
+
return operations;
|
|
3822
3829
|
};
|
|
3823
3830
|
|
|
3824
|
-
const
|
|
3825
|
-
return
|
|
3831
|
+
const isDroppedFile = item => {
|
|
3832
|
+
return item.kind === 'file' && 'value' in item && item.value instanceof FileSystemHandle;
|
|
3826
3833
|
};
|
|
3827
|
-
|
|
3828
|
-
const
|
|
3829
|
-
const {
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3834
|
+
const getFileSystemHandlesNormalized = fileSystemHandles => {
|
|
3835
|
+
const normalized = [];
|
|
3836
|
+
for (const item of fileSystemHandles) {
|
|
3837
|
+
if (isDroppedFile(item)) {
|
|
3838
|
+
normalized.push(item.value);
|
|
3839
|
+
} else {
|
|
3840
|
+
normalized.push(item);
|
|
3841
|
+
}
|
|
3835
3842
|
}
|
|
3836
|
-
return
|
|
3843
|
+
return normalized;
|
|
3837
3844
|
};
|
|
3845
|
+
const uploadFileSystemHandles = async (root, pathSeparator, fileSystemHandles) => {
|
|
3846
|
+
if (fileSystemHandles.length === 0) {
|
|
3847
|
+
return true;
|
|
3848
|
+
}
|
|
3849
|
+
const fileSystemHandlesNormalized = getFileSystemHandlesNormalized(fileSystemHandles);
|
|
3850
|
+
const uploadTree = await createUploadTree(root, fileSystemHandlesNormalized);
|
|
3851
|
+
const fileOperations = getFileOperations(root, uploadTree);
|
|
3852
|
+
await applyFileOperations(fileOperations);
|
|
3838
3853
|
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3854
|
+
// TODO
|
|
3855
|
+
// 1. in electron, use webutils.getPathForFile to see if a path is available
|
|
3856
|
+
// 2. else, walk all files and folders recursively and upload all of them (if there are many, show a progress bar)
|
|
3842
3857
|
|
|
3843
|
-
|
|
3844
|
-
return
|
|
3858
|
+
// TODO send file system operations to renderer worker
|
|
3859
|
+
return true;
|
|
3845
3860
|
};
|
|
3846
3861
|
|
|
3847
|
-
const
|
|
3848
|
-
|
|
3849
|
-
|
|
3862
|
+
const mergeDirents$1 = (oldDirents, newDirents) => {
|
|
3863
|
+
return newDirents;
|
|
3864
|
+
};
|
|
3865
|
+
const getMergedDirents$2 = async (root, pathSeparator, dirents) => {
|
|
3866
|
+
const childDirents = await getChildDirents(pathSeparator, root, 0);
|
|
3867
|
+
const mergedDirents = mergeDirents$1(dirents, childDirents);
|
|
3868
|
+
return mergedDirents;
|
|
3869
|
+
};
|
|
3870
|
+
const getDroppedDirectoryWorkspacePath = fileHandle => {
|
|
3871
|
+
return `html://${fileHandle.name}`;
|
|
3872
|
+
};
|
|
3873
|
+
const openDroppedDirectoryAsWorkspace = async (state, fileHandle) => {
|
|
3874
|
+
const path = getDroppedDirectoryWorkspacePath(fileHandle);
|
|
3875
|
+
await invoke$2('PersistentFileHandle.addHandle', fileHandle.name, fileHandle);
|
|
3876
|
+
await invoke$2('Workspace.setPath', path);
|
|
3877
|
+
const updated = await loadContent(state, undefined);
|
|
3878
|
+
return {
|
|
3879
|
+
...updated,
|
|
3880
|
+
dropTargets: []
|
|
3881
|
+
};
|
|
3882
|
+
};
|
|
3883
|
+
const getFirstDroppedDirectory = (state, fileHandles) => {
|
|
3884
|
+
if (state.root !== '') {
|
|
3885
|
+
return undefined;
|
|
3850
3886
|
}
|
|
3851
|
-
const
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
matches.push(i);
|
|
3887
|
+
for (const fileHandle of fileHandles) {
|
|
3888
|
+
if (isDirectoryHandle(fileHandle)) {
|
|
3889
|
+
return fileHandle;
|
|
3855
3890
|
}
|
|
3856
3891
|
}
|
|
3857
|
-
|
|
3858
|
-
return -1;
|
|
3859
|
-
}
|
|
3860
|
-
|
|
3861
|
-
// Find the next match after the current focus
|
|
3862
|
-
let nextIndex = matches.findIndex(index => index > focusedIndex);
|
|
3863
|
-
if (nextIndex === -1) {
|
|
3864
|
-
// If no match found after current focus, wrap around to the first match
|
|
3865
|
-
nextIndex = 0;
|
|
3866
|
-
}
|
|
3867
|
-
return matches[nextIndex];
|
|
3892
|
+
return undefined;
|
|
3868
3893
|
};
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
const isAscii = key => {
|
|
3872
|
-
return RE_ASCII.test(key);
|
|
3894
|
+
const shouldIgnoreDroppedHandles = (state, fileHandles) => {
|
|
3895
|
+
return state.root === '' && fileHandles.length > 0 && !getFirstDroppedDirectory(state, fileHandles);
|
|
3873
3896
|
};
|
|
3874
|
-
|
|
3875
|
-
let timeout;
|
|
3876
|
-
const handleKeyDown = (state, key) => {
|
|
3897
|
+
const handleDrop$2 = async (state, fileHandles, files) => {
|
|
3877
3898
|
const {
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
items
|
|
3899
|
+
items,
|
|
3900
|
+
pathSeparator,
|
|
3901
|
+
root
|
|
3882
3902
|
} = state;
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
if (!isAscii(key)) {
|
|
3887
|
-
return state;
|
|
3888
|
-
}
|
|
3889
|
-
const newFocusWord = focusWord + key.toLowerCase();
|
|
3890
|
-
const itemNames = items.map(item => item.name);
|
|
3891
|
-
const matchingIndex = filterByFocusWord(itemNames, focusedIndex, newFocusWord);
|
|
3892
|
-
if (timeout) {
|
|
3893
|
-
clearTimeout(timeout);
|
|
3903
|
+
const droppedDirectory = getFirstDroppedDirectory(state, fileHandles);
|
|
3904
|
+
if (droppedDirectory) {
|
|
3905
|
+
return openDroppedDirectoryAsWorkspace(state, droppedDirectory);
|
|
3894
3906
|
}
|
|
3895
|
-
|
|
3896
|
-
await invoke$2('Explorer.cancelTypeAhead');
|
|
3897
|
-
}, focusWordTimeout);
|
|
3898
|
-
if (matchingIndex === -1) {
|
|
3907
|
+
if (shouldIgnoreDroppedHandles(state, fileHandles)) {
|
|
3899
3908
|
return {
|
|
3900
3909
|
...state,
|
|
3901
|
-
|
|
3910
|
+
dropTargets: []
|
|
3911
|
+
};
|
|
3912
|
+
}
|
|
3913
|
+
const handled = await uploadFileSystemHandles(root, pathSeparator, fileHandles);
|
|
3914
|
+
if (handled) {
|
|
3915
|
+
const updated = await refresh(state);
|
|
3916
|
+
return {
|
|
3917
|
+
...updated,
|
|
3918
|
+
dropTargets: []
|
|
3902
3919
|
};
|
|
3903
3920
|
}
|
|
3921
|
+
const mergedDirents = await getMergedDirents$2(root, pathSeparator, items);
|
|
3904
3922
|
return {
|
|
3905
3923
|
...state,
|
|
3906
|
-
|
|
3907
|
-
|
|
3924
|
+
dropTargets: [],
|
|
3925
|
+
items: mergedDirents
|
|
3908
3926
|
};
|
|
3909
3927
|
};
|
|
3910
3928
|
|
|
3911
|
-
const
|
|
3912
|
-
const
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
newMinLineY: index - smallerHalf
|
|
3925
|
-
};
|
|
3929
|
+
const getFileOperationsElectron = async (root, paths, fileHandles, pathSeparator) => {
|
|
3930
|
+
const operations = [];
|
|
3931
|
+
for (let i = 0; i < paths.length; i++) {
|
|
3932
|
+
const fileHandle = fileHandles[i];
|
|
3933
|
+
const {
|
|
3934
|
+
name
|
|
3935
|
+
} = fileHandle;
|
|
3936
|
+
const path = paths[i];
|
|
3937
|
+
operations.push({
|
|
3938
|
+
from: path,
|
|
3939
|
+
path: join(pathSeparator, root, name),
|
|
3940
|
+
type: Copy$1
|
|
3941
|
+
});
|
|
3926
3942
|
}
|
|
3927
|
-
return
|
|
3928
|
-
newMaxLineY: maxLineY,
|
|
3929
|
-
newMinLineY: minLineY
|
|
3930
|
-
};
|
|
3943
|
+
return operations;
|
|
3931
3944
|
};
|
|
3932
3945
|
|
|
3933
|
-
|
|
3946
|
+
// TODO copy files in parallel
|
|
3947
|
+
const copyFilesElectron = async (root, fileHandles, files, paths) => {
|
|
3948
|
+
const pathSeparator = await getPathSeparator$1(root);
|
|
3949
|
+
const operations = await getFileOperationsElectron(root, paths, fileHandles, pathSeparator);
|
|
3950
|
+
await applyFileOperations(operations);
|
|
3951
|
+
};
|
|
3952
|
+
|
|
3953
|
+
const mergeDirents = (oldDirents, newDirents) => {
|
|
3954
|
+
return newDirents;
|
|
3955
|
+
};
|
|
3956
|
+
const getMergedDirents$1 = async (root, pathSeparator, dirents) => {
|
|
3957
|
+
const childDirents = await getChildDirents(pathSeparator, root, 0);
|
|
3958
|
+
const mergedDirents = mergeDirents(dirents, childDirents);
|
|
3959
|
+
return mergedDirents;
|
|
3960
|
+
};
|
|
3961
|
+
const handleDrop$1 = async (state, fileHandles, files, paths) => {
|
|
3934
3962
|
const {
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3963
|
+
items,
|
|
3964
|
+
pathSeparator,
|
|
3965
|
+
root
|
|
3938
3966
|
} = state;
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
newMinLineY
|
|
3942
|
-
} = scrollInto(focusedIndex, minLineY, maxLineY);
|
|
3943
|
-
const newDeltaY = newMinLineY * itemHeight;
|
|
3967
|
+
await copyFilesElectron(root, fileHandles, files, paths);
|
|
3968
|
+
const mergedDirents = await getMergedDirents$1(root, pathSeparator, items);
|
|
3944
3969
|
return {
|
|
3945
3970
|
...state,
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
focusedIndex,
|
|
3949
|
-
maxLineY: newMaxLineY,
|
|
3950
|
-
minLineY: newMinLineY
|
|
3971
|
+
dropTargets: [],
|
|
3972
|
+
items: mergedDirents
|
|
3951
3973
|
};
|
|
3952
3974
|
};
|
|
3953
3975
|
|
|
3954
|
-
const
|
|
3955
|
-
// Handle files with extensions
|
|
3956
|
-
const lastDotIndex = baseName.lastIndexOf('.');
|
|
3957
|
-
const hasExtension = lastDotIndex !== -1 && lastDotIndex !== 0 && lastDotIndex !== baseName.length - 1;
|
|
3958
|
-
let nameWithoutExtension;
|
|
3959
|
-
let extension;
|
|
3960
|
-
if (hasExtension) {
|
|
3961
|
-
nameWithoutExtension = baseName.slice(0, lastDotIndex);
|
|
3962
|
-
extension = baseName.slice(lastDotIndex);
|
|
3963
|
-
} else {
|
|
3964
|
-
nameWithoutExtension = baseName;
|
|
3965
|
-
extension = '';
|
|
3966
|
-
}
|
|
3967
|
-
|
|
3968
|
-
// Check if original name exists
|
|
3969
|
-
const originalPath = join2(root, baseName);
|
|
3970
|
-
if (!existingPaths.includes(originalPath)) {
|
|
3971
|
-
return baseName;
|
|
3972
|
-
}
|
|
3976
|
+
const Electron = 2;
|
|
3973
3977
|
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
if (!existingPaths.includes(copyPath)) {
|
|
3978
|
-
return copyName;
|
|
3978
|
+
const getModule = isElectron => {
|
|
3979
|
+
if (isElectron) {
|
|
3980
|
+
return handleDrop$1;
|
|
3979
3981
|
}
|
|
3982
|
+
return handleDrop$2;
|
|
3983
|
+
};
|
|
3984
|
+
const handleDropRoot = async (state, fileHandles, files, paths) => {
|
|
3985
|
+
const isElectron = state.platform === Electron;
|
|
3986
|
+
const fn = getModule(isElectron);
|
|
3987
|
+
return fn(state, fileHandles, files, paths);
|
|
3988
|
+
};
|
|
3980
3989
|
|
|
3981
|
-
|
|
3982
|
-
let
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
const numberedCopyPath = join2(root, numberedCopyName);
|
|
3986
|
-
if (!existingPaths.includes(numberedCopyPath)) {
|
|
3987
|
-
return numberedCopyName;
|
|
3990
|
+
const getEndIndex = (items, index, dirent) => {
|
|
3991
|
+
for (let i = index + 1; i < items.length; i++) {
|
|
3992
|
+
if (items[i].depth === dirent.depth) {
|
|
3993
|
+
return i;
|
|
3988
3994
|
}
|
|
3989
|
-
counter++;
|
|
3990
3995
|
}
|
|
3996
|
+
return items.length;
|
|
3991
3997
|
};
|
|
3992
|
-
|
|
3993
|
-
const
|
|
3994
|
-
const
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
path: join2(focusedUri, baseName),
|
|
4001
|
-
type: Rename$2
|
|
4002
|
-
});
|
|
4003
|
-
} else {
|
|
4004
|
-
const uniqueName = generateUniqueName(baseName, existingUris, root);
|
|
4005
|
-
const newUri = join2(root, uniqueName);
|
|
4006
|
-
operations.push({
|
|
4007
|
-
from: file,
|
|
4008
|
-
// TODO ensure file is uri
|
|
4009
|
-
path: newUri,
|
|
4010
|
-
type: Copy$1
|
|
4011
|
-
});
|
|
4012
|
-
}
|
|
4013
|
-
}
|
|
4014
|
-
return operations;
|
|
3998
|
+
const getMergedDirents = (items, index, dirent, childDirents) => {
|
|
3999
|
+
const startIndex = index;
|
|
4000
|
+
const endIndex = getEndIndex(items, index, dirent);
|
|
4001
|
+
const mergedDirents = [...items.slice(0, startIndex), {
|
|
4002
|
+
...dirent,
|
|
4003
|
+
type: DirectoryExpanded
|
|
4004
|
+
}, ...childDirents, ...items.slice(endIndex)];
|
|
4005
|
+
return mergedDirents;
|
|
4015
4006
|
};
|
|
4016
|
-
|
|
4017
|
-
const handlePasteCopy = async (state, nativeFiles) => {
|
|
4018
|
-
// TODO handle pasting files into nested folder
|
|
4019
|
-
// TODO handle pasting files into symlink
|
|
4020
|
-
// TODO handle pasting files into broken symlink
|
|
4021
|
-
// TODO handle pasting files into hardlink
|
|
4022
|
-
// TODO what if folder is big and it takes a long time
|
|
4023
|
-
|
|
4024
|
-
// TODO use file operations and bulk edit
|
|
4007
|
+
const handleDropIntoFolder = async (state, dirent, index, fileHandles, files, paths) => {
|
|
4025
4008
|
const {
|
|
4026
|
-
focusedIndex,
|
|
4027
4009
|
items,
|
|
4028
|
-
|
|
4010
|
+
pathSeparator
|
|
4029
4011
|
} = state;
|
|
4030
|
-
|
|
4031
|
-
const
|
|
4032
|
-
const
|
|
4033
|
-
// TODO
|
|
4034
|
-
await applyFileOperations(operations);
|
|
4035
|
-
|
|
4036
|
-
// TODO use refreshExplorer with the paths that have been affected by file operations
|
|
4037
|
-
// TODO only update folder at which level it changed
|
|
4038
|
-
const latestState = await refresh(state);
|
|
4039
|
-
|
|
4040
|
-
// Focus on the first newly created file and adjust scroll position
|
|
4041
|
-
const newFilePaths = operations.map(operation => operation.path);
|
|
4042
|
-
if (newFilePaths.length > 0) {
|
|
4043
|
-
const firstNewFilePath = newFilePaths[0];
|
|
4044
|
-
const newFileIndex = getIndex(latestState.items, firstNewFilePath);
|
|
4045
|
-
if (newFileIndex !== -1) {
|
|
4046
|
-
const adjustedState = adjustScrollAfterPaste(latestState, newFileIndex);
|
|
4047
|
-
return {
|
|
4048
|
-
...adjustedState,
|
|
4049
|
-
pasteShouldMove: false
|
|
4050
|
-
};
|
|
4051
|
-
}
|
|
4052
|
-
}
|
|
4053
|
-
// If there are no items, ensure focusedIndex is 0
|
|
4054
|
-
if (latestState.items.length === 0) {
|
|
4055
|
-
return {
|
|
4056
|
-
...latestState,
|
|
4057
|
-
focusedIndex: 0,
|
|
4058
|
-
pasteShouldMove: false
|
|
4059
|
-
};
|
|
4060
|
-
}
|
|
4012
|
+
await uploadFileSystemHandles(dirent.path, '/', fileHandles);
|
|
4013
|
+
const childDirents = await getChildDirents(pathSeparator, dirent.path, dirent.depth);
|
|
4014
|
+
const mergedDirents = getMergedDirents(items, index, dirent, childDirents);
|
|
4015
|
+
// TODO update maxlineY
|
|
4061
4016
|
return {
|
|
4062
|
-
...
|
|
4063
|
-
|
|
4017
|
+
...state,
|
|
4018
|
+
dropTargets: [],
|
|
4019
|
+
items: mergedDirents
|
|
4064
4020
|
};
|
|
4065
4021
|
};
|
|
4066
|
-
|
|
4067
|
-
const
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
from: file,
|
|
4074
|
-
path: newUri,
|
|
4075
|
-
type: Rename$2
|
|
4076
|
-
});
|
|
4077
|
-
}
|
|
4078
|
-
return operations;
|
|
4079
|
-
};
|
|
4080
|
-
const getTargetUri = (root, items, index) => {
|
|
4081
|
-
if (index === -1) {
|
|
4082
|
-
return root;
|
|
4022
|
+
const handleDropIntoFile = (state, dirent, index, fileHandles, files, paths) => {
|
|
4023
|
+
const {
|
|
4024
|
+
items
|
|
4025
|
+
} = state;
|
|
4026
|
+
const parentIndex = getParentStartIndex(items, index);
|
|
4027
|
+
if (parentIndex === -1) {
|
|
4028
|
+
return handleDropRoot(state, fileHandles, files, paths);
|
|
4083
4029
|
}
|
|
4084
|
-
return
|
|
4030
|
+
return handleDropIndex(state, fileHandles, files, paths, parentIndex);
|
|
4085
4031
|
};
|
|
4086
|
-
const
|
|
4032
|
+
const handleDropIndex = async (state, fileHandles, files, paths, index) => {
|
|
4087
4033
|
const {
|
|
4088
|
-
|
|
4089
|
-
items,
|
|
4090
|
-
pathSeparator,
|
|
4091
|
-
root
|
|
4034
|
+
items
|
|
4092
4035
|
} = state;
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4036
|
+
const dirent = items[index];
|
|
4037
|
+
// TODO if it is a file, drop into the folder of the file
|
|
4038
|
+
// TODO if it is a folder, drop into the folder
|
|
4039
|
+
// TODO if it is a symlink, read symlink and determine if file can be dropped
|
|
4040
|
+
switch (dirent.type) {
|
|
4041
|
+
case Directory:
|
|
4042
|
+
case DirectoryExpanded:
|
|
4043
|
+
return handleDropIntoFolder(state, dirent, index, fileHandles);
|
|
4044
|
+
case File:
|
|
4045
|
+
return handleDropIntoFile(state, dirent, index, fileHandles, files, paths);
|
|
4046
|
+
default:
|
|
4047
|
+
return state;
|
|
4048
|
+
}
|
|
4049
|
+
};
|
|
4100
4050
|
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
const adjustedState = adjustScrollAfterPaste(latestState, pastedFileIndex);
|
|
4108
|
-
return {
|
|
4109
|
-
...adjustedState,
|
|
4110
|
-
cutItems: [],
|
|
4111
|
-
pasteShouldMove: false
|
|
4112
|
-
};
|
|
4113
|
-
}
|
|
4051
|
+
const getDropHandler = index => {
|
|
4052
|
+
switch (index) {
|
|
4053
|
+
case -1:
|
|
4054
|
+
return handleDropRoot;
|
|
4055
|
+
default:
|
|
4056
|
+
return handleDropIndex;
|
|
4114
4057
|
}
|
|
4115
|
-
return {
|
|
4116
|
-
...latestState,
|
|
4117
|
-
cutItems: [],
|
|
4118
|
-
pasteShouldMove: false
|
|
4119
|
-
};
|
|
4120
4058
|
};
|
|
4121
4059
|
|
|
4122
|
-
const
|
|
4060
|
+
const getFileArray = fileList => {
|
|
4061
|
+
// @ts-ignore
|
|
4062
|
+
const files = [...fileList];
|
|
4063
|
+
return files;
|
|
4064
|
+
};
|
|
4123
4065
|
|
|
4124
|
-
const
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
return state;
|
|
4066
|
+
const getFileHandles = async fileIds => {
|
|
4067
|
+
if (fileIds.length === 0) {
|
|
4068
|
+
return [];
|
|
4128
4069
|
}
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
// TODO what happens when pasting multiple paths, but some of them error?
|
|
4133
|
-
// how many error messages should be shown? Should the operation be undone?
|
|
4134
|
-
// TODO what if it is a large folder and takes a long time to copy? Should show progress
|
|
4135
|
-
// TODO what if there is a permission error? Probably should show a modal to ask for permission
|
|
4136
|
-
// TODO if error is EEXISTS, just rename the copy (e.g. file-copy-1.txt, file-copy-2.txt)
|
|
4137
|
-
// TODO actual target should be selected folder
|
|
4138
|
-
// TODO but what if a file is currently selected? Then maybe the parent folder
|
|
4139
|
-
// TODO but will it work if the folder is a symlink?
|
|
4140
|
-
// TODO handle error gracefully when copy fails
|
|
4070
|
+
const files = await getFileHandles$1(fileIds);
|
|
4071
|
+
return files;
|
|
4072
|
+
};
|
|
4141
4073
|
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
}
|
|
4074
|
+
const getFilePathElectron = async file => {
|
|
4075
|
+
return invoke$2('FileSystemHandle.getFilePathElectron', file);
|
|
4076
|
+
};
|
|
4146
4077
|
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4078
|
+
const getFilepath = async file => {
|
|
4079
|
+
return getFilePathElectron(file);
|
|
4080
|
+
};
|
|
4081
|
+
const getFilePaths = async (files, platform) => {
|
|
4082
|
+
if (platform !== Electron) {
|
|
4083
|
+
return files.map(file => '');
|
|
4150
4084
|
}
|
|
4151
|
-
|
|
4085
|
+
const promises = files.map(getFilepath);
|
|
4086
|
+
const paths = await Promise.all(promises);
|
|
4087
|
+
return paths;
|
|
4152
4088
|
};
|
|
4153
4089
|
|
|
4154
|
-
const
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4090
|
+
const handleDrop = async (state, x, y, fileIds, fileList) => {
|
|
4091
|
+
try {
|
|
4092
|
+
const {
|
|
4093
|
+
platform
|
|
4094
|
+
} = state;
|
|
4095
|
+
const files = getFileArray(fileList);
|
|
4096
|
+
const fileHandles = await getFileHandles(fileIds);
|
|
4097
|
+
const paths = await getFilePaths(files, platform);
|
|
4098
|
+
const index = getIndexFromPosition(state, x, y);
|
|
4099
|
+
const fn = getDropHandler(index);
|
|
4100
|
+
const result = await fn(state, fileHandles, files, paths, index);
|
|
4101
|
+
return result;
|
|
4102
|
+
} catch (error) {
|
|
4103
|
+
throw new VError(error, 'Failed to drop files');
|
|
4163
4104
|
}
|
|
4164
|
-
return state;
|
|
4165
4105
|
};
|
|
4166
4106
|
|
|
4167
|
-
const
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4107
|
+
const handleEscape = async state => {
|
|
4108
|
+
return {
|
|
4109
|
+
...state,
|
|
4110
|
+
cutItems: []
|
|
4111
|
+
};
|
|
4172
4112
|
};
|
|
4173
4113
|
|
|
4174
|
-
const
|
|
4114
|
+
const handleFocus = async state => {
|
|
4115
|
+
return {
|
|
4116
|
+
...state,
|
|
4117
|
+
focus: List
|
|
4118
|
+
};
|
|
4119
|
+
};
|
|
4120
|
+
|
|
4121
|
+
const updateIcons = async state => {
|
|
4175
4122
|
const {
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
if (!Number.isFinite(rawHeight) || !Number.isFinite(rawWidth)) {
|
|
4180
|
-
return state;
|
|
4181
|
-
}
|
|
4182
|
-
const height = Math.max(0, rawHeight);
|
|
4183
|
-
const width = Math.max(0, rawWidth);
|
|
4184
|
-
const {
|
|
4185
|
-
deltaY,
|
|
4186
|
-
itemHeight,
|
|
4187
|
-
items
|
|
4123
|
+
items,
|
|
4124
|
+
maxLineY,
|
|
4125
|
+
minLineY
|
|
4188
4126
|
} = state;
|
|
4189
|
-
const
|
|
4190
|
-
const
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
const scrollBarHeight = getScrollBarSize(height, contentHeight, 20);
|
|
4195
|
-
if (state.height === height && state.width === width && state.deltaY === newDeltaY && state.minLineY === minLineY && state.maxLineY === maxLineY && state.scrollBarHeight === scrollBarHeight) {
|
|
4196
|
-
return state;
|
|
4197
|
-
}
|
|
4127
|
+
const visible = items.slice(minLineY, maxLineY);
|
|
4128
|
+
const {
|
|
4129
|
+
icons,
|
|
4130
|
+
newFileIconCache
|
|
4131
|
+
} = await getFileIcons(visible, Object.create(null));
|
|
4198
4132
|
return {
|
|
4199
4133
|
...state,
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
maxLineY,
|
|
4203
|
-
minLineY,
|
|
4204
|
-
scrollBarHeight,
|
|
4205
|
-
width
|
|
4134
|
+
fileIconCache: newFileIconCache,
|
|
4135
|
+
icons
|
|
4206
4136
|
};
|
|
4207
4137
|
};
|
|
4208
4138
|
|
|
4209
|
-
const
|
|
4139
|
+
const handleIconThemeChange = state => {
|
|
4140
|
+
return updateIcons(state);
|
|
4141
|
+
};
|
|
4142
|
+
|
|
4143
|
+
const handleInputBlur = async state => {
|
|
4210
4144
|
const {
|
|
4211
|
-
|
|
4212
|
-
|
|
4145
|
+
editingErrorMessage,
|
|
4146
|
+
editingValue
|
|
4213
4147
|
} = state;
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
// TODO symlink might not be possible to be copied
|
|
4217
|
-
// TODO create folder if type is 2
|
|
4218
|
-
if (dirent.type === /* File */1) {
|
|
4219
|
-
// TODO reading text might be inefficient for binary files
|
|
4220
|
-
// but not sure how else to send them via jsonrpc
|
|
4221
|
-
const content = await dirent.file.text();
|
|
4222
|
-
const absolutePath = [root, dirent.file.name].join(pathSeparator);
|
|
4223
|
-
await writeFile(absolutePath, content);
|
|
4224
|
-
}
|
|
4148
|
+
if (editingErrorMessage || !editingValue) {
|
|
4149
|
+
return cancelEditInternal(state, false);
|
|
4225
4150
|
}
|
|
4151
|
+
return acceptEdit(state);
|
|
4226
4152
|
};
|
|
4227
4153
|
|
|
4228
|
-
const
|
|
4229
|
-
|
|
4230
|
-
|
|
4154
|
+
const handleInputClick = state => {
|
|
4155
|
+
return state;
|
|
4156
|
+
};
|
|
4157
|
+
|
|
4158
|
+
const handleInputKeyDown = (state, key) => {
|
|
4159
|
+
return state;
|
|
4160
|
+
};
|
|
4161
|
+
|
|
4162
|
+
const filterByFocusWord = (items, focusedIndex, focusWord) => {
|
|
4163
|
+
if (items.length === 0) {
|
|
4164
|
+
return -1;
|
|
4165
|
+
}
|
|
4166
|
+
const matches = [];
|
|
4167
|
+
for (let i = 0; i < items.length; i++) {
|
|
4168
|
+
if (items[i].toLowerCase().includes(focusWord)) {
|
|
4169
|
+
matches.push(i);
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
if (matches.length === 0) {
|
|
4173
|
+
return -1;
|
|
4231
4174
|
}
|
|
4175
|
+
|
|
4176
|
+
// Find the next match after the current focus
|
|
4177
|
+
let nextIndex = matches.findIndex(index => index > focusedIndex);
|
|
4178
|
+
if (nextIndex === -1) {
|
|
4179
|
+
// If no match found after current focus, wrap around to the first match
|
|
4180
|
+
nextIndex = 0;
|
|
4181
|
+
}
|
|
4182
|
+
return matches[nextIndex];
|
|
4183
|
+
};
|
|
4184
|
+
|
|
4185
|
+
const RE_ASCII = /^[a-z]$/;
|
|
4186
|
+
const isAscii = key => {
|
|
4187
|
+
return RE_ASCII.test(key);
|
|
4188
|
+
};
|
|
4189
|
+
|
|
4190
|
+
let timeout;
|
|
4191
|
+
const handleKeyDown = (state, key) => {
|
|
4232
4192
|
const {
|
|
4233
|
-
|
|
4234
|
-
|
|
4193
|
+
focusedIndex,
|
|
4194
|
+
focusWord,
|
|
4195
|
+
focusWordTimeout,
|
|
4235
4196
|
items
|
|
4236
4197
|
} = state;
|
|
4237
|
-
if (
|
|
4238
|
-
|
|
4239
|
-
} else if (deltaY > items.length * itemHeight - height) {
|
|
4240
|
-
deltaY = Math.max(items.length * itemHeight - height, 0);
|
|
4198
|
+
if (focusWord && key === '') {
|
|
4199
|
+
return cancelTypeAhead(state);
|
|
4241
4200
|
}
|
|
4242
|
-
if (
|
|
4201
|
+
if (!isAscii(key)) {
|
|
4243
4202
|
return state;
|
|
4244
4203
|
}
|
|
4245
|
-
const
|
|
4246
|
-
const
|
|
4204
|
+
const newFocusWord = focusWord + key.toLowerCase();
|
|
4205
|
+
const itemNames = items.map(item => item.name);
|
|
4206
|
+
const matchingIndex = filterByFocusWord(itemNames, focusedIndex, newFocusWord);
|
|
4207
|
+
if (timeout) {
|
|
4208
|
+
clearTimeout(timeout);
|
|
4209
|
+
}
|
|
4210
|
+
timeout = setTimeout(async () => {
|
|
4211
|
+
await invoke$2('Explorer.cancelTypeAhead');
|
|
4212
|
+
}, focusWordTimeout);
|
|
4213
|
+
if (matchingIndex === -1) {
|
|
4214
|
+
return {
|
|
4215
|
+
...state,
|
|
4216
|
+
focusWord: newFocusWord
|
|
4217
|
+
};
|
|
4218
|
+
}
|
|
4247
4219
|
return {
|
|
4248
4220
|
...state,
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
minLineY
|
|
4221
|
+
focusedIndex: matchingIndex,
|
|
4222
|
+
focusWord: newFocusWord
|
|
4252
4223
|
};
|
|
4253
4224
|
};
|
|
4254
4225
|
|
|
4255
|
-
const
|
|
4256
|
-
|
|
4226
|
+
const scrollInto = (index, minLineY, maxLineY) => {
|
|
4227
|
+
const diff = maxLineY - minLineY;
|
|
4228
|
+
const smallerHalf = Math.floor(diff / 2);
|
|
4229
|
+
const largerHalf = diff - smallerHalf;
|
|
4230
|
+
if (index < minLineY) {
|
|
4231
|
+
return {
|
|
4232
|
+
newMaxLineY: index + largerHalf,
|
|
4233
|
+
newMinLineY: index - smallerHalf
|
|
4234
|
+
};
|
|
4235
|
+
}
|
|
4236
|
+
if (index >= maxLineY) {
|
|
4237
|
+
return {
|
|
4238
|
+
newMaxLineY: index + largerHalf,
|
|
4239
|
+
newMinLineY: index - smallerHalf
|
|
4240
|
+
};
|
|
4241
|
+
}
|
|
4242
|
+
return {
|
|
4243
|
+
newMaxLineY: maxLineY,
|
|
4244
|
+
newMinLineY: minLineY
|
|
4245
|
+
};
|
|
4257
4246
|
};
|
|
4258
4247
|
|
|
4259
|
-
const
|
|
4260
|
-
|
|
4248
|
+
const adjustScrollAfterPaste = (state, focusedIndex) => {
|
|
4249
|
+
const {
|
|
4250
|
+
itemHeight,
|
|
4251
|
+
maxLineY,
|
|
4252
|
+
minLineY
|
|
4253
|
+
} = state;
|
|
4254
|
+
const {
|
|
4255
|
+
newMaxLineY,
|
|
4256
|
+
newMinLineY
|
|
4257
|
+
} = scrollInto(focusedIndex, minLineY, maxLineY);
|
|
4258
|
+
const newDeltaY = newMinLineY * itemHeight;
|
|
4259
|
+
return {
|
|
4260
|
+
...state,
|
|
4261
|
+
deltaY: newDeltaY,
|
|
4262
|
+
focused: true,
|
|
4263
|
+
focusedIndex,
|
|
4264
|
+
maxLineY: newMaxLineY,
|
|
4265
|
+
minLineY: newMinLineY
|
|
4266
|
+
};
|
|
4261
4267
|
};
|
|
4262
4268
|
|
|
4263
|
-
const
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
const
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
+
const generateUniqueName = (baseName, existingPaths, root) => {
|
|
4270
|
+
// Handle files with extensions
|
|
4271
|
+
const lastDotIndex = baseName.lastIndexOf('.');
|
|
4272
|
+
const hasExtension = lastDotIndex !== -1 && lastDotIndex !== 0 && lastDotIndex !== baseName.length - 1;
|
|
4273
|
+
let nameWithoutExtension;
|
|
4274
|
+
let extension;
|
|
4275
|
+
if (hasExtension) {
|
|
4276
|
+
nameWithoutExtension = baseName.slice(0, lastDotIndex);
|
|
4277
|
+
extension = baseName.slice(lastDotIndex);
|
|
4278
|
+
} else {
|
|
4279
|
+
nameWithoutExtension = baseName;
|
|
4280
|
+
extension = '';
|
|
4269
4281
|
}
|
|
4270
|
-
return decorations.filter(isValid);
|
|
4271
|
-
};
|
|
4272
4282
|
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4283
|
+
// Check if original name exists
|
|
4284
|
+
const originalPath = join2(root, baseName);
|
|
4285
|
+
if (!existingPaths.includes(originalPath)) {
|
|
4286
|
+
return baseName;
|
|
4287
|
+
}
|
|
4288
|
+
|
|
4289
|
+
// Try "original copy"
|
|
4290
|
+
const copyName = `${nameWithoutExtension} copy${extension}`;
|
|
4291
|
+
const copyPath = join2(root, copyName);
|
|
4292
|
+
if (!existingPaths.includes(copyPath)) {
|
|
4293
|
+
return copyName;
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
// Try "original copy 1", "original copy 2", etc.
|
|
4297
|
+
let counter = 1;
|
|
4298
|
+
while (true) {
|
|
4299
|
+
const numberedCopyName = `${nameWithoutExtension} copy ${counter}${extension}`;
|
|
4300
|
+
const numberedCopyPath = join2(root, numberedCopyName);
|
|
4301
|
+
if (!existingPaths.includes(numberedCopyPath)) {
|
|
4302
|
+
return numberedCopyName;
|
|
4281
4303
|
}
|
|
4282
|
-
|
|
4283
|
-
const providerId = providerIds.at(-1);
|
|
4284
|
-
const uris = ensureUris(maybeUris);
|
|
4285
|
-
const decorations = await invoke$1('SourceControl.getFileDecorations', providerId, uris, assetDir, platform);
|
|
4286
|
-
const normalized = normalizeDecorations(decorations);
|
|
4287
|
-
return normalized;
|
|
4288
|
-
} catch (error) {
|
|
4289
|
-
console.error(error);
|
|
4290
|
-
return [];
|
|
4304
|
+
counter++;
|
|
4291
4305
|
}
|
|
4292
4306
|
};
|
|
4293
4307
|
|
|
4294
|
-
const
|
|
4295
|
-
const
|
|
4296
|
-
const
|
|
4297
|
-
|
|
4298
|
-
|
|
4308
|
+
const getFileOperationsCopy = (root, existingUris, files, focusedUri) => {
|
|
4309
|
+
const operations = [];
|
|
4310
|
+
for (const file of files) {
|
|
4311
|
+
const baseName = getBaseName('/', file);
|
|
4312
|
+
if (existingUris.includes(file)) {
|
|
4313
|
+
operations.push({
|
|
4314
|
+
from: file,
|
|
4315
|
+
path: join2(focusedUri, baseName),
|
|
4316
|
+
type: Rename$2
|
|
4317
|
+
});
|
|
4318
|
+
} else {
|
|
4319
|
+
const uniqueName = generateUniqueName(baseName, existingUris, root);
|
|
4320
|
+
const newUri = join2(root, uniqueName);
|
|
4321
|
+
operations.push({
|
|
4322
|
+
from: file,
|
|
4323
|
+
// TODO ensure file is uri
|
|
4324
|
+
path: newUri,
|
|
4325
|
+
type: Copy$1
|
|
4326
|
+
});
|
|
4327
|
+
}
|
|
4299
4328
|
}
|
|
4300
|
-
return
|
|
4329
|
+
return operations;
|
|
4301
4330
|
};
|
|
4302
4331
|
|
|
4303
|
-
const
|
|
4304
|
-
// TODO
|
|
4305
|
-
// TODO
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
const
|
|
4312
|
-
|
|
4313
|
-
|
|
4332
|
+
const handlePasteCopy = async (state, nativeFiles) => {
|
|
4333
|
+
// TODO handle pasting files into nested folder
|
|
4334
|
+
// TODO handle pasting files into symlink
|
|
4335
|
+
// TODO handle pasting files into broken symlink
|
|
4336
|
+
// TODO handle pasting files into hardlink
|
|
4337
|
+
// TODO what if folder is big and it takes a long time
|
|
4338
|
+
|
|
4339
|
+
// TODO use file operations and bulk edit
|
|
4340
|
+
const {
|
|
4341
|
+
focusedIndex,
|
|
4342
|
+
items,
|
|
4343
|
+
root
|
|
4344
|
+
} = state;
|
|
4345
|
+
const focusedUri = items[focusedIndex]?.path || root;
|
|
4346
|
+
const existingUris = items.map(item => item.path);
|
|
4347
|
+
const operations = getFileOperationsCopy(root, existingUris, nativeFiles.files, focusedUri);
|
|
4348
|
+
// TODO handle error?
|
|
4349
|
+
await applyFileOperations(operations);
|
|
4350
|
+
|
|
4351
|
+
// TODO use refreshExplorer with the paths that have been affected by file operations
|
|
4352
|
+
// TODO only update folder at which level it changed
|
|
4353
|
+
const latestState = await refresh(state);
|
|
4354
|
+
|
|
4355
|
+
// Focus on the first newly created file and adjust scroll position
|
|
4356
|
+
const newFilePaths = operations.map(operation => operation.path);
|
|
4357
|
+
if (newFilePaths.length > 0) {
|
|
4358
|
+
const firstNewFilePath = newFilePaths[0];
|
|
4359
|
+
const newFileIndex = getIndex(latestState.items, firstNewFilePath);
|
|
4360
|
+
if (newFileIndex !== -1) {
|
|
4361
|
+
const adjustedState = adjustScrollAfterPaste(latestState, newFileIndex);
|
|
4362
|
+
return {
|
|
4363
|
+
...adjustedState,
|
|
4364
|
+
pasteShouldMove: false
|
|
4365
|
+
};
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
// If there are no items, ensure focusedIndex is 0
|
|
4369
|
+
if (latestState.items.length === 0) {
|
|
4370
|
+
return {
|
|
4371
|
+
...latestState,
|
|
4372
|
+
focusedIndex: 0,
|
|
4373
|
+
pasteShouldMove: false
|
|
4374
|
+
};
|
|
4375
|
+
}
|
|
4314
4376
|
return {
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
sourceControlDecorations,
|
|
4318
|
-
useChevrons
|
|
4377
|
+
...latestState,
|
|
4378
|
+
pasteShouldMove: false
|
|
4319
4379
|
};
|
|
4320
4380
|
};
|
|
4321
4381
|
|
|
4322
|
-
const
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
if (excluded.includes(child.name)) {
|
|
4333
|
-
continue;
|
|
4334
|
-
}
|
|
4335
|
-
visible.push(child);
|
|
4382
|
+
const getOperations = (toUri, files) => {
|
|
4383
|
+
const operations = [];
|
|
4384
|
+
for (const file of files) {
|
|
4385
|
+
const baseName = getBaseName('/', file);
|
|
4386
|
+
const newUri = join2(toUri, baseName);
|
|
4387
|
+
operations.push({
|
|
4388
|
+
from: file,
|
|
4389
|
+
path: newUri,
|
|
4390
|
+
type: Rename$2
|
|
4391
|
+
});
|
|
4336
4392
|
}
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
type
|
|
4343
|
-
} = child;
|
|
4344
|
-
const childPath = displayRoot + name;
|
|
4345
|
-
if ((child.type === Directory || child.type === SymLinkFolder) && childPath in map) {
|
|
4346
|
-
dirents.push({
|
|
4347
|
-
depth,
|
|
4348
|
-
icon: '',
|
|
4349
|
-
name,
|
|
4350
|
-
path: childPath,
|
|
4351
|
-
posInSet: i + 1,
|
|
4352
|
-
setSize: visibleLength,
|
|
4353
|
-
type: DirectoryExpanded
|
|
4354
|
-
});
|
|
4355
|
-
dirents.push(...getSavedChildDirents(map, childPath, depth + 1, excluded, pathSeparator));
|
|
4356
|
-
} else {
|
|
4357
|
-
dirents.push({
|
|
4358
|
-
depth,
|
|
4359
|
-
icon: '',
|
|
4360
|
-
name,
|
|
4361
|
-
path: childPath,
|
|
4362
|
-
posInSet: i + 1,
|
|
4363
|
-
setSize: visibleLength,
|
|
4364
|
-
type
|
|
4365
|
-
});
|
|
4366
|
-
}
|
|
4393
|
+
return operations;
|
|
4394
|
+
};
|
|
4395
|
+
const getTargetUri = (root, items, index) => {
|
|
4396
|
+
if (index === -1) {
|
|
4397
|
+
return root;
|
|
4367
4398
|
}
|
|
4368
|
-
return
|
|
4399
|
+
return items[index].path;
|
|
4369
4400
|
};
|
|
4401
|
+
const handlePasteCut = async (state, nativeFiles) => {
|
|
4402
|
+
const {
|
|
4403
|
+
focusedIndex,
|
|
4404
|
+
items,
|
|
4405
|
+
pathSeparator,
|
|
4406
|
+
root
|
|
4407
|
+
} = state;
|
|
4408
|
+
// TODO root is not necessrily target uri
|
|
4409
|
+
const targetUri = getTargetUri(root, items, focusedIndex);
|
|
4410
|
+
const operations = getOperations(targetUri, nativeFiles.files);
|
|
4411
|
+
await applyFileOperations(operations);
|
|
4370
4412
|
|
|
4371
|
-
|
|
4372
|
-
const
|
|
4413
|
+
// Refresh the state after cut operations
|
|
4414
|
+
const latestState = await refresh(state);
|
|
4373
4415
|
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
const
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4416
|
+
// Focus on the first pasted file and adjust scroll position
|
|
4417
|
+
if (nativeFiles.files.length > 0) {
|
|
4418
|
+
const firstPastedFile = nativeFiles.files[0];
|
|
4419
|
+
const targetPath = `${root}${pathSeparator}${getBaseName(pathSeparator, firstPastedFile)}`;
|
|
4420
|
+
const pastedFileIndex = getIndex(latestState.items, targetPath);
|
|
4421
|
+
if (pastedFileIndex !== -1) {
|
|
4422
|
+
const adjustedState = adjustScrollAfterPaste(latestState, pastedFileIndex);
|
|
4423
|
+
return {
|
|
4424
|
+
...adjustedState,
|
|
4425
|
+
cutItems: [],
|
|
4426
|
+
pasteShouldMove: false
|
|
4427
|
+
};
|
|
4382
4428
|
}
|
|
4383
4429
|
}
|
|
4384
|
-
|
|
4385
|
-
|
|
4430
|
+
return {
|
|
4431
|
+
...latestState,
|
|
4432
|
+
cutItems: [],
|
|
4433
|
+
pasteShouldMove: false
|
|
4434
|
+
};
|
|
4386
4435
|
};
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4436
|
+
|
|
4437
|
+
const None$1 = 'none';
|
|
4438
|
+
|
|
4439
|
+
const handlePaste = async state => {
|
|
4440
|
+
const nativeFiles = await readNativeFiles();
|
|
4441
|
+
if (!nativeFiles) {
|
|
4442
|
+
return state;
|
|
4393
4443
|
}
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
// TODO
|
|
4398
|
-
//
|
|
4399
|
-
//
|
|
4400
|
-
//
|
|
4401
|
-
//
|
|
4402
|
-
|
|
4403
|
-
if
|
|
4404
|
-
|
|
4444
|
+
// TODO detect cut/paste event, not sure if that is possible
|
|
4445
|
+
// TODO check that pasted folder is not a parent folder of opened folder
|
|
4446
|
+
// TODO support pasting multiple paths
|
|
4447
|
+
// TODO what happens when pasting multiple paths, but some of them error?
|
|
4448
|
+
// how many error messages should be shown? Should the operation be undone?
|
|
4449
|
+
// TODO what if it is a large folder and takes a long time to copy? Should show progress
|
|
4450
|
+
// TODO what if there is a permission error? Probably should show a modal to ask for permission
|
|
4451
|
+
// TODO if error is EEXISTS, just rename the copy (e.g. file-copy-1.txt, file-copy-2.txt)
|
|
4452
|
+
// TODO actual target should be selected folder
|
|
4453
|
+
// TODO but what if a file is currently selected? Then maybe the parent folder
|
|
4454
|
+
// TODO but will it work if the folder is a symlink?
|
|
4455
|
+
// TODO handle error gracefully when copy fails
|
|
4456
|
+
|
|
4457
|
+
// If no files to paste, return original state unchanged
|
|
4458
|
+
if (nativeFiles.type === None$1) {
|
|
4459
|
+
return state;
|
|
4405
4460
|
}
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
if (
|
|
4409
|
-
|
|
4461
|
+
|
|
4462
|
+
// Use the pasteShouldMove flag to determine whether to cut or copy
|
|
4463
|
+
if (state.pasteShouldMove) {
|
|
4464
|
+
return handlePasteCut(state, nativeFiles);
|
|
4410
4465
|
}
|
|
4411
|
-
|
|
4412
|
-
return dirents;
|
|
4466
|
+
return handlePasteCopy(state, nativeFiles);
|
|
4413
4467
|
};
|
|
4414
4468
|
|
|
4415
|
-
const
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
}
|
|
4469
|
+
const handlePointerDown = (state, button, x, y) => {
|
|
4470
|
+
const index = getIndexFromPosition(state, x, y);
|
|
4471
|
+
if (button === LeftClick && index === -1) {
|
|
4472
|
+
return {
|
|
4473
|
+
...state,
|
|
4474
|
+
focus: List,
|
|
4475
|
+
focused: true,
|
|
4476
|
+
focusedIndex: -1
|
|
4477
|
+
};
|
|
4425
4478
|
}
|
|
4426
|
-
return
|
|
4427
|
-
};
|
|
4428
|
-
const getSavedRoot = (savedState, workspacePath) => {
|
|
4429
|
-
return workspacePath;
|
|
4479
|
+
return state;
|
|
4430
4480
|
};
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4481
|
+
|
|
4482
|
+
const getScrollBarSize = (size, contentSize, minimumSliderSize) => {
|
|
4483
|
+
if (size >= contentSize) {
|
|
4484
|
+
return 0;
|
|
4434
4485
|
}
|
|
4435
|
-
return
|
|
4486
|
+
return Math.max(Math.round(size ** 2 / contentSize), minimumSliderSize);
|
|
4436
4487
|
};
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4488
|
+
|
|
4489
|
+
const handleResize = (state, dimensions) => {
|
|
4490
|
+
const {
|
|
4491
|
+
height: rawHeight,
|
|
4492
|
+
width: rawWidth
|
|
4493
|
+
} = dimensions;
|
|
4494
|
+
if (!Number.isFinite(rawHeight) || !Number.isFinite(rawWidth)) {
|
|
4495
|
+
return state;
|
|
4440
4496
|
}
|
|
4441
|
-
|
|
4442
|
-
|
|
4497
|
+
const height = Math.max(0, rawHeight);
|
|
4498
|
+
const width = Math.max(0, rawWidth);
|
|
4499
|
+
const {
|
|
4500
|
+
deltaY,
|
|
4501
|
+
itemHeight,
|
|
4502
|
+
items
|
|
4503
|
+
} = state;
|
|
4504
|
+
const contentHeight = items.length * itemHeight;
|
|
4505
|
+
const maxDeltaY = Math.max(contentHeight - height, 0);
|
|
4506
|
+
const newDeltaY = Math.min(Math.max(deltaY, 0), maxDeltaY);
|
|
4507
|
+
const minLineY = Math.round(newDeltaY / itemHeight);
|
|
4508
|
+
const maxLineY = getExplorerMaxLineY(minLineY, height, itemHeight, items.length);
|
|
4509
|
+
const scrollBarHeight = getScrollBarSize(height, contentHeight, 20);
|
|
4510
|
+
if (state.height === height && state.width === width && state.deltaY === newDeltaY && state.minLineY === minLineY && state.maxLineY === maxLineY && state.scrollBarHeight === scrollBarHeight) {
|
|
4511
|
+
return state;
|
|
4443
4512
|
}
|
|
4444
|
-
return
|
|
4513
|
+
return {
|
|
4514
|
+
...state,
|
|
4515
|
+
deltaY: newDeltaY,
|
|
4516
|
+
height,
|
|
4517
|
+
maxLineY,
|
|
4518
|
+
minLineY,
|
|
4519
|
+
scrollBarHeight,
|
|
4520
|
+
width
|
|
4521
|
+
};
|
|
4445
4522
|
};
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4523
|
+
|
|
4524
|
+
const handleUpload = async (state, dirents) => {
|
|
4525
|
+
const {
|
|
4526
|
+
pathSeparator,
|
|
4527
|
+
root
|
|
4528
|
+
} = state;
|
|
4529
|
+
for (const dirent of dirents) {
|
|
4530
|
+
// TODO switch
|
|
4531
|
+
// TODO symlink might not be possible to be copied
|
|
4532
|
+
// TODO create folder if type is 2
|
|
4533
|
+
if (dirent.type === /* File */1) {
|
|
4534
|
+
// TODO reading text might be inefficient for binary files
|
|
4535
|
+
// but not sure how else to send them via jsonrpc
|
|
4536
|
+
const content = await dirent.file.text();
|
|
4537
|
+
const absolutePath = [root, dirent.file.name].join(pathSeparator);
|
|
4538
|
+
await writeFile(absolutePath, content);
|
|
4539
|
+
}
|
|
4459
4540
|
}
|
|
4460
4541
|
};
|
|
4461
|
-
|
|
4542
|
+
|
|
4543
|
+
const setDeltaY = async (state, deltaY) => {
|
|
4544
|
+
if (!Number.isFinite(deltaY)) {
|
|
4545
|
+
return state;
|
|
4546
|
+
}
|
|
4462
4547
|
const {
|
|
4463
|
-
|
|
4464
|
-
|
|
4548
|
+
height,
|
|
4549
|
+
itemHeight,
|
|
4550
|
+
items
|
|
4465
4551
|
} = state;
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
} = await getSettings();
|
|
4471
|
-
const workspacePath = await getWorkspacePath();
|
|
4472
|
-
const root = getSavedRoot(savedState, workspacePath);
|
|
4473
|
-
try {
|
|
4474
|
-
// TODO path separator could be restored from saved state
|
|
4475
|
-
const pathSeparator = await getPathSeparator(root); // TODO only load path separator once
|
|
4476
|
-
const excluded = getExcluded();
|
|
4477
|
-
const restoredDirents = await restoreExpandedState(savedState, root, pathSeparator, excluded);
|
|
4478
|
-
let minLineY = 0;
|
|
4479
|
-
if (savedState && typeof savedState.minLineY === 'number') {
|
|
4480
|
-
minLineY = savedState.minLineY;
|
|
4481
|
-
}
|
|
4482
|
-
let deltaY = 0;
|
|
4483
|
-
if (savedState && typeof savedState.deltaY === 'number') {
|
|
4484
|
-
deltaY = savedState.deltaY;
|
|
4485
|
-
}
|
|
4486
|
-
const scheme = getScheme(root);
|
|
4487
|
-
const decorations = await getFileDecorations(scheme, root, restoredDirents.filter(item => item.depth === 1).map(item => item.path), sourceControlDecorations, assetDir, platform);
|
|
4488
|
-
return {
|
|
4489
|
-
...state,
|
|
4490
|
-
confirmDelete,
|
|
4491
|
-
decorations,
|
|
4492
|
-
deltaY,
|
|
4493
|
-
errorCode: '',
|
|
4494
|
-
errorMessage: '',
|
|
4495
|
-
excluded,
|
|
4496
|
-
hasError: false,
|
|
4497
|
-
initial: false,
|
|
4498
|
-
items: restoredDirents,
|
|
4499
|
-
maxIndent: 10,
|
|
4500
|
-
minLineY,
|
|
4501
|
-
pathSeparator,
|
|
4502
|
-
root,
|
|
4503
|
-
useChevrons
|
|
4504
|
-
};
|
|
4505
|
-
} catch (error) {
|
|
4506
|
-
const errorCode = getErrorCode(error);
|
|
4507
|
-
const errorMessage = getFriendlyErrorMessage(getErrorMessage(error), errorCode);
|
|
4508
|
-
return {
|
|
4509
|
-
...state,
|
|
4510
|
-
confirmDelete,
|
|
4511
|
-
errorCode,
|
|
4512
|
-
errorMessage,
|
|
4513
|
-
hasError: true,
|
|
4514
|
-
initial: false,
|
|
4515
|
-
items: [],
|
|
4516
|
-
root,
|
|
4517
|
-
useChevrons
|
|
4518
|
-
};
|
|
4552
|
+
if (deltaY < 0) {
|
|
4553
|
+
deltaY = 0;
|
|
4554
|
+
} else if (deltaY > items.length * itemHeight - height) {
|
|
4555
|
+
deltaY = Math.max(items.length * itemHeight - height, 0);
|
|
4519
4556
|
}
|
|
4557
|
+
if (state.deltaY === deltaY) {
|
|
4558
|
+
return state;
|
|
4559
|
+
}
|
|
4560
|
+
const minLineY = Math.round(deltaY / itemHeight);
|
|
4561
|
+
const maxLineY = minLineY + Math.round(height / itemHeight);
|
|
4562
|
+
return {
|
|
4563
|
+
...state,
|
|
4564
|
+
deltaY,
|
|
4565
|
+
maxLineY,
|
|
4566
|
+
minLineY
|
|
4567
|
+
};
|
|
4568
|
+
};
|
|
4569
|
+
|
|
4570
|
+
const handleWheel = (state, deltaMode, deltaY) => {
|
|
4571
|
+
return setDeltaY(state, state.deltaY + deltaY);
|
|
4520
4572
|
};
|
|
4521
4573
|
|
|
4522
4574
|
const handleWorkspaceChange = async state => {
|
|
@@ -4884,10 +4936,17 @@ const HandlePointerDown = 14;
|
|
|
4884
4936
|
const HandleWheel = 15;
|
|
4885
4937
|
const HandleDoubleClick = 16;
|
|
4886
4938
|
|
|
4887
|
-
const
|
|
4939
|
+
const getClassName$1 = dropTargets => {
|
|
4940
|
+
const extraClassName = dropTargets === dropTargetFull ? ExplorerDropTarget : Empty;
|
|
4941
|
+
return mergeClassNames(Viewlet, Explorer$1, extraClassName);
|
|
4942
|
+
};
|
|
4943
|
+
const getExplorerWelcomeVirtualDom = (isWide, dropTargets) => {
|
|
4888
4944
|
return [{
|
|
4889
4945
|
childCount: 1,
|
|
4890
|
-
className:
|
|
4946
|
+
className: getClassName$1(dropTargets),
|
|
4947
|
+
onDragLeave: HandleDragLeave,
|
|
4948
|
+
onDragOver: HandleDragOver,
|
|
4949
|
+
onDrop: HandleDrop,
|
|
4891
4950
|
tabIndex: 0,
|
|
4892
4951
|
type: Div
|
|
4893
4952
|
}, {
|
|
@@ -5110,7 +5169,7 @@ const getChildCount = (scrollBarDomLength, errorDomLength) => {
|
|
|
5110
5169
|
};
|
|
5111
5170
|
const getExplorerVirtualDom = (visibleItems, focusedIndex, root, isWide, focused, dropTargets, height, contentHeight, editingErrorMessage, loadErrorMessage) => {
|
|
5112
5171
|
if (!root) {
|
|
5113
|
-
return getExplorerWelcomeVirtualDom(isWide);
|
|
5172
|
+
return getExplorerWelcomeVirtualDom(isWide, dropTargets);
|
|
5114
5173
|
}
|
|
5115
5174
|
if (loadErrorMessage) {
|
|
5116
5175
|
return getLoadErrorVirtualDom(loadErrorMessage);
|