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