@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.
@@ -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 isFileHandle = fileHandle => {
3521
- return fileHandle.kind === 'file';
3507
+ const isValid = decoration => {
3508
+ return decoration && typeof decoration.decoration === 'string' && typeof decoration.uri === 'string';
3522
3509
  };
3523
-
3524
- const createUploadTree = async (root, fileHandles) => {
3525
- const uploadTree = Object.create(null);
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 uploadTree;
3514
+ return decorations.filter(isValid);
3542
3515
  };
3543
3516
 
3544
- const getFileOperations = (root, uploadTree) => {
3545
- const operations = [];
3546
- const processTree = (tree, currentPath) => {
3547
- for (const [path, value] of Object.entries(tree)) {
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
- processTree(uploadTree, '');
3565
- return operations;
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 uploadFileSystemHandles = async (root, pathSeparator, fileSystemHandles) => {
3569
- if (fileSystemHandles.length === 0) {
3570
- return true;
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
- const uploadTree = await createUploadTree(root, fileSystemHandles);
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 mergeDirents$1 = (oldDirents, newDirents) => {
3585
- return newDirents;
3586
- };
3587
- const getMergedDirents$2 = async (root, pathSeparator, dirents) => {
3588
- const childDirents = await getChildDirents(pathSeparator, root, 0);
3589
- const mergedDirents = mergeDirents$1(dirents, childDirents);
3590
- return mergedDirents;
3591
- };
3592
- const handleDrop$2 = async (state, fileHandles, files) => {
3593
- const {
3594
- items,
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
- ...state,
3609
- dropTargets: [],
3610
- items: mergedDirents
3559
+ confirmDelete,
3560
+ confirmPaste,
3561
+ sourceControlDecorations,
3562
+ useChevrons
3611
3563
  };
3612
3564
  };
3613
3565
 
3614
- const getFileOperationsElectron = async (root, paths, fileHandles, pathSeparator) => {
3615
- const operations = [];
3616
- for (let i = 0; i < paths.length; i++) {
3617
- const fileHandle = fileHandles[i];
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
- } = fileHandle;
3621
- const path = paths[i];
3622
- operations.push({
3623
- from: path,
3624
- path: join(pathSeparator, root, name),
3625
- type: Copy$1
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 operations;
3616
+ return dirents;
3629
3617
  };
3630
3618
 
3631
- // TODO copy files in parallel
3632
- const copyFilesElectron = async (root, fileHandles, files, paths) => {
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 mergeDirents = (oldDirents, newDirents) => {
3639
- return newDirents;
3640
- };
3641
- const getMergedDirents$1 = async (root, pathSeparator, dirents) => {
3642
- const childDirents = await getChildDirents(pathSeparator, root, 0);
3643
- const mergedDirents = mergeDirents(dirents, childDirents);
3644
- return mergedDirents;
3645
- };
3646
- const handleDrop$1 = async (state, fileHandles, files, paths) => {
3647
- const {
3648
- items,
3649
- pathSeparator,
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
- const Electron = 2;
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
- return handleDrop$2;
3639
+ if (savedState && savedState.expandedPaths && Array.isArray(savedState.expandedPaths)) {
3640
+ return savedState.expandedPaths;
3641
+ }
3642
+ return [];
3668
3643
  };
3669
- const handleDropRoot = async (state, fileHandles, files, paths) => {
3670
- const isElectron = state.platform === Electron;
3671
- const fn = getModule(isElectron);
3672
- return fn(state, fileHandles, files, paths);
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 getEndIndex = (items, index, dirent) => {
3676
- for (let i = index + 1; i < items.length; i++) {
3677
- if (items[i].depth === dirent.depth) {
3678
- return i;
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 items.length;
3674
+ return excluded;
3682
3675
  };
3683
- const getMergedDirents = (items, index, dirent, childDirents) => {
3684
- const startIndex = index;
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 handleDropIntoFolder = async (state, dirent, index, fileHandles, files, paths) => {
3693
- const {
3694
- items,
3695
- pathSeparator
3696
- } = state;
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 handleDropIntoFile = (state, dirent, index, fileHandles, files, paths) => {
3708
- const {
3709
- items
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
- return handleDropIndex(state, fileHandles, files, paths, parentIndex);
3689
+ if (typeof error === 'string') {
3690
+ return error;
3691
+ }
3692
+ return 'Unknown error';
3716
3693
  };
3717
- const handleDropIndex = async (state, fileHandles, files, paths, index) => {
3718
- const {
3719
- items
3720
- } = state;
3721
- const dirent = items[index];
3722
- // TODO if it is a file, drop into the folder of the file
3723
- // TODO if it is a folder, drop into the folder
3724
- // TODO if it is a symlink, read symlink and determine if file can be dropped
3725
- switch (dirent.type) {
3726
- case Directory:
3727
- case DirectoryExpanded:
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 state;
3706
+ return errorMessage || 'an unexpected error occurred';
3733
3707
  }
3734
3708
  };
3735
-
3736
- const getDropHandler = index => {
3737
- switch (index) {
3738
- case -1:
3739
- return handleDropRoot;
3740
- default:
3741
- return handleDropIndex;
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 getFileArray = fileList => {
3770
+ const getChildHandles = async fileHandle => {
3746
3771
  // @ts-ignore
3747
- const files = [...fileList];
3748
- return files;
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 getFilePathElectron = async file => {
3760
- return invoke$2('FileSystemHandle.getFilePathElectron', file);
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 getFilepath = async file => {
3764
- return getFilePathElectron(file);
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 handleDrop = async (state, x, y, fileIds, fileList) => {
3776
- try {
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
- platform
3779
- } = state;
3780
- const files = getFileArray(fileList);
3781
- const fileHandles = await getFileHandles(fileIds);
3782
- const paths = await getFilePaths(files, platform);
3783
- const index = getIndexFromPosition(state, x, y);
3784
- const fn = getDropHandler(index);
3785
- const result = await fn(state, fileHandles, files, paths, index);
3786
- return result;
3787
- } catch (error) {
3788
- throw new VError(error, 'Failed to drop files');
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 handleEscape = async state => {
3793
- return {
3794
- ...state,
3795
- cutItems: []
3796
- };
3797
- };
3798
-
3799
- const handleFocus = async state => {
3800
- return {
3801
- ...state,
3802
- focus: List
3803
- };
3804
- };
3805
-
3806
- const updateIcons = async state => {
3807
- const {
3808
- items,
3809
- maxLineY,
3810
- minLineY
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 handleIconThemeChange = state => {
3825
- return updateIcons(state);
3831
+ const isDroppedFile = item => {
3832
+ return item.kind === 'file' && 'value' in item && item.value instanceof FileSystemHandle;
3826
3833
  };
3827
-
3828
- const handleInputBlur = async state => {
3829
- const {
3830
- editingErrorMessage,
3831
- editingValue
3832
- } = state;
3833
- if (editingErrorMessage || !editingValue) {
3834
- return cancelEditInternal(state, false);
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 acceptEdit(state);
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
- const handleInputClick = state => {
3840
- return state;
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
- const handleInputKeyDown = (state, key) => {
3844
- return state;
3858
+ // TODO send file system operations to renderer worker
3859
+ return true;
3845
3860
  };
3846
3861
 
3847
- const filterByFocusWord = (items, focusedIndex, focusWord) => {
3848
- if (items.length === 0) {
3849
- return -1;
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 matches = [];
3852
- for (let i = 0; i < items.length; i++) {
3853
- if (items[i].toLowerCase().includes(focusWord)) {
3854
- matches.push(i);
3887
+ for (const fileHandle of fileHandles) {
3888
+ if (isDirectoryHandle(fileHandle)) {
3889
+ return fileHandle;
3855
3890
  }
3856
3891
  }
3857
- if (matches.length === 0) {
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
- const RE_ASCII = /^[a-z]$/;
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
- focusedIndex,
3879
- focusWord,
3880
- focusWordTimeout,
3881
- items
3899
+ items,
3900
+ pathSeparator,
3901
+ root
3882
3902
  } = state;
3883
- if (focusWord && key === '') {
3884
- return cancelTypeAhead(state);
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
- timeout = setTimeout(async () => {
3896
- await invoke$2('Explorer.cancelTypeAhead');
3897
- }, focusWordTimeout);
3898
- if (matchingIndex === -1) {
3907
+ if (shouldIgnoreDroppedHandles(state, fileHandles)) {
3899
3908
  return {
3900
3909
  ...state,
3901
- focusWord: newFocusWord
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
- focusedIndex: matchingIndex,
3907
- focusWord: newFocusWord
3924
+ dropTargets: [],
3925
+ items: mergedDirents
3908
3926
  };
3909
3927
  };
3910
3928
 
3911
- const scrollInto = (index, minLineY, maxLineY) => {
3912
- const diff = maxLineY - minLineY;
3913
- const smallerHalf = Math.floor(diff / 2);
3914
- const largerHalf = diff - smallerHalf;
3915
- if (index < minLineY) {
3916
- return {
3917
- newMaxLineY: index + largerHalf,
3918
- newMinLineY: index - smallerHalf
3919
- };
3920
- }
3921
- if (index >= maxLineY) {
3922
- return {
3923
- newMaxLineY: index + largerHalf,
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
- const adjustScrollAfterPaste = (state, focusedIndex) => {
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
- itemHeight,
3936
- maxLineY,
3937
- minLineY
3963
+ items,
3964
+ pathSeparator,
3965
+ root
3938
3966
  } = state;
3939
- const {
3940
- newMaxLineY,
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
- deltaY: newDeltaY,
3947
- focused: true,
3948
- focusedIndex,
3949
- maxLineY: newMaxLineY,
3950
- minLineY: newMinLineY
3971
+ dropTargets: [],
3972
+ items: mergedDirents
3951
3973
  };
3952
3974
  };
3953
3975
 
3954
- const generateUniqueName = (baseName, existingPaths, root) => {
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
- // Try "original copy"
3975
- const copyName = `${nameWithoutExtension} copy${extension}`;
3976
- const copyPath = join2(root, copyName);
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
- // Try "original copy 1", "original copy 2", etc.
3982
- let counter = 1;
3983
- while (true) {
3984
- const numberedCopyName = `${nameWithoutExtension} copy ${counter}${extension}`;
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 getFileOperationsCopy = (root, existingUris, files, focusedUri) => {
3994
- const operations = [];
3995
- for (const file of files) {
3996
- const baseName = getBaseName('/', file);
3997
- if (existingUris.includes(file)) {
3998
- operations.push({
3999
- from: file,
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
- root
4010
+ pathSeparator
4029
4011
  } = state;
4030
- const focusedUri = items[focusedIndex]?.path || root;
4031
- const existingUris = items.map(item => item.path);
4032
- const operations = getFileOperationsCopy(root, existingUris, nativeFiles.files, focusedUri);
4033
- // TODO handle error?
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
- ...latestState,
4063
- pasteShouldMove: false
4017
+ ...state,
4018
+ dropTargets: [],
4019
+ items: mergedDirents
4064
4020
  };
4065
4021
  };
4066
-
4067
- const getOperations = (toUri, files) => {
4068
- const operations = [];
4069
- for (const file of files) {
4070
- const baseName = getBaseName('/', file);
4071
- const newUri = join2(toUri, baseName);
4072
- operations.push({
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 items[index].path;
4030
+ return handleDropIndex(state, fileHandles, files, paths, parentIndex);
4085
4031
  };
4086
- const handlePasteCut = async (state, nativeFiles) => {
4032
+ const handleDropIndex = async (state, fileHandles, files, paths, index) => {
4087
4033
  const {
4088
- focusedIndex,
4089
- items,
4090
- pathSeparator,
4091
- root
4034
+ items
4092
4035
  } = state;
4093
- // TODO root is not necessrily target uri
4094
- const targetUri = getTargetUri(root, items, focusedIndex);
4095
- const operations = getOperations(targetUri, nativeFiles.files);
4096
- await applyFileOperations(operations);
4097
-
4098
- // Refresh the state after cut operations
4099
- const latestState = await refresh(state);
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
- // Focus on the first pasted file and adjust scroll position
4102
- if (nativeFiles.files.length > 0) {
4103
- const firstPastedFile = nativeFiles.files[0];
4104
- const targetPath = `${root}${pathSeparator}${getBaseName(pathSeparator, firstPastedFile)}`;
4105
- const pastedFileIndex = getIndex(latestState.items, targetPath);
4106
- if (pastedFileIndex !== -1) {
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 None$1 = 'none';
4060
+ const getFileArray = fileList => {
4061
+ // @ts-ignore
4062
+ const files = [...fileList];
4063
+ return files;
4064
+ };
4123
4065
 
4124
- const handlePaste = async state => {
4125
- const nativeFiles = await readNativeFiles();
4126
- if (!nativeFiles) {
4127
- return state;
4066
+ const getFileHandles = async fileIds => {
4067
+ if (fileIds.length === 0) {
4068
+ return [];
4128
4069
  }
4129
- // TODO detect cut/paste event, not sure if that is possible
4130
- // TODO check that pasted folder is not a parent folder of opened folder
4131
- // TODO support pasting multiple paths
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
- // If no files to paste, return original state unchanged
4143
- if (nativeFiles.type === None$1) {
4144
- return state;
4145
- }
4074
+ const getFilePathElectron = async file => {
4075
+ return invoke$2('FileSystemHandle.getFilePathElectron', file);
4076
+ };
4146
4077
 
4147
- // Use the pasteShouldMove flag to determine whether to cut or copy
4148
- if (state.pasteShouldMove) {
4149
- return handlePasteCut(state, nativeFiles);
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
- return handlePasteCopy(state, nativeFiles);
4085
+ const promises = files.map(getFilepath);
4086
+ const paths = await Promise.all(promises);
4087
+ return paths;
4152
4088
  };
4153
4089
 
4154
- const handlePointerDown = (state, button, x, y) => {
4155
- const index = getIndexFromPosition(state, x, y);
4156
- if (button === LeftClick && index === -1) {
4157
- return {
4158
- ...state,
4159
- focus: List,
4160
- focused: true,
4161
- focusedIndex: -1
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 getScrollBarSize = (size, contentSize, minimumSliderSize) => {
4168
- if (size >= contentSize) {
4169
- return 0;
4170
- }
4171
- return Math.max(Math.round(size ** 2 / contentSize), minimumSliderSize);
4107
+ const handleEscape = async state => {
4108
+ return {
4109
+ ...state,
4110
+ cutItems: []
4111
+ };
4172
4112
  };
4173
4113
 
4174
- const handleResize = (state, dimensions) => {
4114
+ const handleFocus = async state => {
4115
+ return {
4116
+ ...state,
4117
+ focus: List
4118
+ };
4119
+ };
4120
+
4121
+ const updateIcons = async state => {
4175
4122
  const {
4176
- height: rawHeight,
4177
- width: rawWidth
4178
- } = dimensions;
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 contentHeight = items.length * itemHeight;
4190
- const maxDeltaY = Math.max(contentHeight - height, 0);
4191
- const newDeltaY = Math.min(Math.max(deltaY, 0), maxDeltaY);
4192
- const minLineY = Math.round(newDeltaY / itemHeight);
4193
- const maxLineY = getExplorerMaxLineY(minLineY, height, itemHeight, items.length);
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
- deltaY: newDeltaY,
4201
- height,
4202
- maxLineY,
4203
- minLineY,
4204
- scrollBarHeight,
4205
- width
4134
+ fileIconCache: newFileIconCache,
4135
+ icons
4206
4136
  };
4207
4137
  };
4208
4138
 
4209
- const handleUpload = async (state, dirents) => {
4139
+ const handleIconThemeChange = state => {
4140
+ return updateIcons(state);
4141
+ };
4142
+
4143
+ const handleInputBlur = async state => {
4210
4144
  const {
4211
- pathSeparator,
4212
- root
4145
+ editingErrorMessage,
4146
+ editingValue
4213
4147
  } = state;
4214
- for (const dirent of dirents) {
4215
- // TODO switch
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 setDeltaY = async (state, deltaY) => {
4229
- if (!Number.isFinite(deltaY)) {
4230
- return state;
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
- height,
4234
- itemHeight,
4193
+ focusedIndex,
4194
+ focusWord,
4195
+ focusWordTimeout,
4235
4196
  items
4236
4197
  } = state;
4237
- if (deltaY < 0) {
4238
- deltaY = 0;
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 (state.deltaY === deltaY) {
4201
+ if (!isAscii(key)) {
4243
4202
  return state;
4244
4203
  }
4245
- const minLineY = Math.round(deltaY / itemHeight);
4246
- const maxLineY = minLineY + Math.round(height / itemHeight);
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
- deltaY,
4250
- maxLineY,
4251
- minLineY
4221
+ focusedIndex: matchingIndex,
4222
+ focusWord: newFocusWord
4252
4223
  };
4253
4224
  };
4254
4225
 
4255
- const handleWheel = (state, deltaMode, deltaY) => {
4256
- return setDeltaY(state, state.deltaY + deltaY);
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 getWorkspacePath = () => {
4260
- return invoke$2('Workspace.getPath');
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 isValid = decoration => {
4264
- return decoration && typeof decoration.decoration === 'string' && typeof decoration.uri === 'string';
4265
- };
4266
- const normalizeDecorations = decorations => {
4267
- if (!decorations || !Array.isArray(decorations)) {
4268
- return [];
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
- const getFileDecorations = async (scheme, root, maybeUris, decorationsEnabled, assetDir, platform) => {
4274
- try {
4275
- if (!decorationsEnabled) {
4276
- return [];
4277
- }
4278
- const providerIds = await invoke$1('SourceControl.getEnabledProviderIds', scheme, root, assetDir, platform);
4279
- if (providerIds.length === 0) {
4280
- return [];
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
- // TODO how to handle multiple providers?
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 RE_PROTOCOL = /^[a-z+]:\/\//;
4295
- const getScheme = uri => {
4296
- const match = uri.match(RE_PROTOCOL);
4297
- if (!match) {
4298
- return '';
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 match[0];
4329
+ return operations;
4301
4330
  };
4302
4331
 
4303
- const getSettings = async () => {
4304
- // TODO don't return false always
4305
- // TODO get all settings at once
4306
- const useChevronsRaw = await invoke$2('Preferences.get', 'explorer.useChevrons');
4307
- const useChevrons = useChevronsRaw === false ? false : true;
4308
- const confirmDeleteRaw = await invoke$2('Preferences.get', 'explorer.confirmdelete');
4309
- const confirmDelete = confirmDeleteRaw === false ? false : false;
4310
- const confirmPasteRaw = await invoke$2('Preferences.get', 'explorer.confirmpaste');
4311
- const confirmPaste = confirmPasteRaw === false ? false : false;
4312
- const sourceControlDecorationsRaw = await invoke$2('Preferences.get', 'explorer.sourceControlDecorations');
4313
- const sourceControlDecorations = sourceControlDecorationsRaw === false ? false : true;
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
- confirmDelete,
4316
- confirmPaste,
4317
- sourceControlDecorations,
4318
- useChevrons
4377
+ ...latestState,
4378
+ pasteShouldMove: false
4319
4379
  };
4320
4380
  };
4321
4381
 
4322
- const getSavedChildDirents = (map, path, depth, excluded, pathSeparator) => {
4323
- let children = map[path];
4324
- if (!children) {
4325
- return [];
4326
- }
4327
- const dirents = [];
4328
- children = sortExplorerItems(children);
4329
- const visible = [];
4330
- const displayRoot = path.endsWith(pathSeparator) ? path : path + pathSeparator;
4331
- for (const child of children) {
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
- const visibleLength = visible.length;
4338
- for (let i = 0; i < visibleLength; i++) {
4339
- const child = visible[i];
4340
- const {
4341
- name,
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 dirents;
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
- const Fulfilled = 'fulfilled';
4372
- const Rejected = 'rejected';
4413
+ // Refresh the state after cut operations
4414
+ const latestState = await refresh(state);
4373
4415
 
4374
- const createDirents = (root, expandedDirentPaths, expandedDirentChildren, excluded, pathSeparator) => {
4375
- const dirents = [];
4376
- const map = Object.create(null);
4377
- for (let i = 0; i < expandedDirentPaths.length; i++) {
4378
- const path = expandedDirentPaths[i];
4379
- const children = expandedDirentChildren[i];
4380
- if (children.status === Fulfilled) {
4381
- map[path] = children.value;
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
- dirents.push(...getSavedChildDirents(map, root, 1, excluded, pathSeparator));
4385
- return dirents;
4430
+ return {
4431
+ ...latestState,
4432
+ cutItems: [],
4433
+ pasteShouldMove: false
4434
+ };
4386
4435
  };
4387
- const getSavedExpandedPaths = (savedState, root) => {
4388
- if (savedState && savedState.root !== root) {
4389
- return [];
4390
- }
4391
- if (savedState && savedState.expandedPaths && Array.isArray(savedState.expandedPaths)) {
4392
- return savedState.expandedPaths;
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
- return [];
4395
- };
4396
- const restoreExpandedState = async (savedState, root, pathSeparator, excluded) => {
4397
- // TODO read all opened folders in parallel
4398
- // ignore ENOENT errors
4399
- // ignore ENOTDIR errors
4400
- // merge all dirents
4401
- // restore scroll location
4402
- const expandedPaths = getSavedExpandedPaths(savedState, root);
4403
- if (root === EmptyString) {
4404
- return [];
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
- const expandedDirentPaths = [root, ...expandedPaths];
4407
- const expandedDirentChildren = await Promise.allSettled(expandedDirentPaths.map(getChildDirentsRaw));
4408
- if (expandedDirentChildren[0].status === Rejected) {
4409
- throw expandedDirentChildren[0].reason;
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
- const dirents = createDirents(root, expandedDirentPaths, expandedDirentChildren, excluded, pathSeparator);
4412
- return dirents;
4466
+ return handlePasteCopy(state, nativeFiles);
4413
4467
  };
4414
4468
 
4415
- const getPathSeparator = root => {
4416
- return getPathSeparator$1(root);
4417
- };
4418
- const getExcluded = () => {
4419
- const excludedObject = {};
4420
- const excluded = [];
4421
- for (const [key, value] of Object.entries(excludedObject)) {
4422
- if (value) {
4423
- excluded.push(key);
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 excluded;
4427
- };
4428
- const getSavedRoot = (savedState, workspacePath) => {
4429
- return workspacePath;
4479
+ return state;
4430
4480
  };
4431
- const getErrorCode = error => {
4432
- if (error && typeof error === 'object' && 'code' in error && typeof error.code === 'string') {
4433
- return error.code;
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
- const getErrorMessage = error => {
4438
- if (error instanceof Error) {
4439
- return error.message;
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
- if (typeof error === 'string') {
4442
- return error;
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 'Unknown error';
4513
+ return {
4514
+ ...state,
4515
+ deltaY: newDeltaY,
4516
+ height,
4517
+ maxLineY,
4518
+ minLineY,
4519
+ scrollBarHeight,
4520
+ width
4521
+ };
4445
4522
  };
4446
- const getFriendlyErrorMessage = (errorMessage, errorCode) => {
4447
- switch (errorCode) {
4448
- case 'EACCES':
4449
- case 'EPERM':
4450
- return 'permission was denied';
4451
- case 'EBUSY':
4452
- return 'the folder is currently in use';
4453
- case 'ENOENT':
4454
- return 'the folder does not exist';
4455
- case 'ENOTDIR':
4456
- return 'the path is not a folder';
4457
- default:
4458
- return errorMessage || 'an unexpected error occurred';
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
- const loadContent = async (state, savedState) => {
4542
+
4543
+ const setDeltaY = async (state, deltaY) => {
4544
+ if (!Number.isFinite(deltaY)) {
4545
+ return state;
4546
+ }
4462
4547
  const {
4463
- assetDir,
4464
- platform
4548
+ height,
4549
+ itemHeight,
4550
+ items
4465
4551
  } = state;
4466
- const {
4467
- confirmDelete,
4468
- sourceControlDecorations,
4469
- useChevrons
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 getExplorerWelcomeVirtualDom = isWide => {
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: mergeClassNames(Viewlet, Explorer$1),
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);