@lvce-editor/explorer-view 2.28.0 → 2.30.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.
@@ -985,8 +985,8 @@ const getMissingIconRequests = (dirents, fileIconCache) => {
985
985
  return missingRequests;
986
986
  };
987
987
 
988
- const getPath = dirent => {
989
- return dirent.path;
988
+ const getPath = item => {
989
+ return item.path;
990
990
  };
991
991
 
992
992
  const DELTA_EDITING = 100;
@@ -1474,6 +1474,13 @@ const cancelEdit = state => {
1474
1474
  };
1475
1475
  };
1476
1476
 
1477
+ const cancelTypeAhead = state => {
1478
+ return {
1479
+ ...state,
1480
+ focusWord: ''
1481
+ };
1482
+ };
1483
+
1477
1484
  const isTopLevel$1 = dirent => {
1478
1485
  return dirent.depth === 1;
1479
1486
  };
@@ -1580,6 +1587,20 @@ const create$1 = () => {
1580
1587
  };
1581
1588
  };
1582
1589
  return wrapped;
1590
+ },
1591
+ diff(uid, modules, numbers) {
1592
+ const {
1593
+ oldState,
1594
+ newState
1595
+ } = states[uid];
1596
+ const diffResult = [];
1597
+ for (let i = 0; i < modules.length; i++) {
1598
+ const fn = modules[i];
1599
+ if (!fn(oldState, newState)) {
1600
+ diffResult.push(numbers[i]);
1601
+ }
1602
+ }
1603
+ return diffResult;
1583
1604
  }
1584
1605
  };
1585
1606
  };
@@ -1630,7 +1651,9 @@ const create2 = (uid, uri, x, y, width, height, args, parentUid, platform = 0) =
1630
1651
  editingSelection: {
1631
1652
  start: 0,
1632
1653
  end: 0
1633
- }
1654
+ },
1655
+ focusWord: '',
1656
+ focusWordTimeout: 800
1634
1657
  };
1635
1658
  set(uid, state, state);
1636
1659
  };
@@ -1671,7 +1694,9 @@ const create = (id, uri, x, y, width, height, args, parentUid, platform = 0) =>
1671
1694
  editingSelection: {
1672
1695
  start: 0,
1673
1696
  end: 0
1674
- }
1697
+ },
1698
+ focusWord: '',
1699
+ focusWordTimeout: 800
1675
1700
  };
1676
1701
  set(state.uid, state, state);
1677
1702
  return state;
@@ -1999,7 +2024,7 @@ const focusIndex = (state, index) => {
1999
2024
  } = state;
2000
2025
  const newItems = items.map((item, i) => ({
2001
2026
  ...item,
2002
- selected: i === index ? true : false
2027
+ selected: i === index ? false : false
2003
2028
  }));
2004
2029
  if (index < minLineY) {
2005
2030
  if (index < 0) {
@@ -2112,7 +2137,7 @@ const focusPrevious = state => {
2112
2137
  }
2113
2138
  };
2114
2139
 
2115
- const commandIds = ['acceptEdit', 'cancelEdit', 'collapseAll', 'copyPath', 'copyRelativePath', 'dispose', 'expandAll', 'expandRecursively', 'focus', 'focusFirst', 'focusIndex', 'focusLast', 'focusNext', 'focusNone', 'focusPrevious', 'getFocusedDirent', 'getMenuEntries2', 'getMouseActions', 'handleArrowLeft', 'handleArrowLeft', 'handleArrowRight', 'handleArrowRight', 'handleBlur', 'handleClick', 'handleClickAt', 'handleClickCurrent', 'handleClickCurrentButKeepFocus', 'handleClickOpenFolder', 'handleContextMenu', 'handleContextMenuKeyboard', 'handleCopy', 'handleDragLeave', 'handleDragOver', 'handleDrop', 'handleFocus', 'handleIconThemeChange', 'handleInputBlur', 'handleInputClick', 'handleLanguagesChanged', 'handleMouseEnter', 'handleMouseLeave', 'handlePaste', 'handlePointerDown', 'handleUpload', 'handleWheel', 'handleWorkspaceChange', 'hotReload', 'newFile', 'newFolder', 'openContainingFolder', 'refresh', 'removeDirent', 'rename', 'renameDirent', 'renderEventListeners', 'revealItem', 'revealItem', 'scrollDown', 'scrollUp', 'selectAll', 'selectDown', 'selectUp', 'setDeltaY', 'setSelectedIndices', 'updateEditingValue', 'updateIcons'];
2140
+ const commandIds = ['acceptEdit', 'cancelEdit', 'collapseAll', 'copyPath', 'copyRelativePath', 'dispose', 'expandAll', 'expandRecursively', 'focus', 'focusFirst', 'focusIndex', 'focusLast', 'focusNext', 'focusNone', 'focusPrevious', 'getFocusedDirent', 'getMenuEntries2', 'getMouseActions', 'handleArrowLeft', 'handleArrowLeft', 'handleArrowRight', 'handleArrowRight', 'handleBlur', 'handleClick', 'handleClickAt', 'handleClickCurrent', 'handleClickCurrentButKeepFocus', 'handleClickOpenFolder', 'handleContextMenu', 'handleContextMenuKeyboard', 'handleCopy', 'handleDragLeave', 'handleDragOver', 'handleDrop', 'handleFocus', 'handleIconThemeChange', 'handleInputBlur', 'handleInputClick', 'handleLanguagesChanged', 'handleMouseEnter', 'handleMouseLeave', 'handlePaste', 'handlePointerDown', 'handleUpload', 'handleWheel', 'handleWorkspaceChange', 'hotReload', 'newFile', 'newFolder', 'openContainingFolder', 'refresh', 'removeDirent', 'rename', 'renameDirent', 'renderEventListeners', 'revealItem', 'revealItem', 'scrollDown', 'scrollUp', 'selectAll', 'selectDown', 'selectUp', 'setDeltaY', 'setSelectedIndices', 'cancelTypeAhead', 'updateEditingValue', 'handleKeyDown', 'updateIcons'];
2116
2141
 
2117
2142
  const getCommandIds = () => {
2118
2143
  return commandIds;
@@ -2943,23 +2968,41 @@ const handleDragOver = (state, x, y) => {
2943
2968
  };
2944
2969
  };
2945
2970
 
2946
- const getTopLevelDirents = (root, pathSeparator, excluded) => {
2947
- if (!root) {
2948
- return [];
2949
- }
2950
- return getChildDirents(pathSeparator, {
2951
- depth: 0,
2952
- path: root,
2953
- type: Directory
2954
- }, excluded);
2971
+ const isExpanded = item => {
2972
+ return item.type === DirectoryExpanded || item.type === DirectoryExpanding;
2955
2973
  };
2956
2974
 
2957
- const mergeDirents$2 = (oldDirents, newDirents) => {
2958
- const merged = [];
2959
- for (const newDirent of newDirents) {
2960
- merged.push(newDirent);
2975
+ const getExpandedDirents = items => {
2976
+ return items.filter(isExpanded);
2977
+ };
2978
+
2979
+ const getPaths = items => {
2980
+ return items.map(getPath);
2981
+ };
2982
+
2983
+ const refreshChildDirent = async (folder, dirent, pathSeparator, expandedFolders) => {
2984
+ const path = folder.path.endsWith(pathSeparator) ? `${folder.path}${dirent.name}` : `${folder.path}${pathSeparator}${dirent.name}`;
2985
+ const isExpandedFolder = expandedFolders.includes(path);
2986
+ const type = dirent.type === 'directory' ? isExpandedFolder ? DirectoryExpanded : Directory : File;
2987
+ const item = {
2988
+ name: dirent.name,
2989
+ type,
2990
+ path,
2991
+ depth: folder.depth + 1,
2992
+ selected: false
2993
+ };
2994
+ if (isExpandedFolder) {
2995
+ const nestedItems = await refreshChildDirents(item, pathSeparator, expandedFolders);
2996
+ return [item, ...nestedItems];
2961
2997
  }
2962
- return merged;
2998
+ return [item];
2999
+ };
3000
+ const refreshChildDirents = async (folder, pathSeparator, expandedFolders) => {
3001
+ const childDirents = await readDirWithFileTypes(folder.path);
3002
+ const childItems = await Promise.all(childDirents.map(async dirent => {
3003
+ return refreshChildDirent(folder, dirent, pathSeparator, expandedFolders);
3004
+ }));
3005
+ return childItems.flat();
2963
3006
  };
2964
3007
 
2965
3008
  // TODO add lots of tests for this
@@ -2970,24 +3013,73 @@ const refresh = async state => {
2970
3013
  minLineY,
2971
3014
  height,
2972
3015
  itemHeight,
2973
- fileIconCache
3016
+ fileIconCache,
3017
+ items,
3018
+ focusedIndex
2974
3019
  } = state;
2975
- const topLevelDirents = await getTopLevelDirents(root, pathSeparator, []);
2976
- const newDirents = mergeDirents$2(state.items, topLevelDirents);
3020
+
3021
+ // Get all expanded folders
3022
+ const expandedDirents = getExpandedDirents(items);
3023
+ const expandedFolders = getPaths(expandedDirents);
3024
+
3025
+ // Get top level dirents
3026
+ const topLevelDirents = await readDirWithFileTypes(root);
3027
+ const newDirents = topLevelDirents.map(dirent => ({
3028
+ name: dirent.name,
3029
+ type: dirent.type === 'directory' ? Directory : File,
3030
+ path: root.endsWith(pathSeparator) ? `${root}${dirent.name}` : `${root}${pathSeparator}${dirent.name}`,
3031
+ depth: 0,
3032
+ selected: false
3033
+ }));
3034
+
3035
+ // Process expanded folders in parallel
3036
+ const expandedFolderResults = await Promise.all(expandedFolders.map(async folderPath => {
3037
+ const folderIndex = newDirents.findIndex(item => item.path === folderPath);
3038
+ if (folderIndex !== -1) {
3039
+ const folder = newDirents[folderIndex];
3040
+ if (folder.type === Directory) {
3041
+ const childItems = await refreshChildDirents(folder, pathSeparator, expandedFolders);
3042
+ // @ts-ignore
3043
+ folder.type = DirectoryExpanded;
3044
+ return {
3045
+ folderIndex,
3046
+ childItems
3047
+ };
3048
+ }
3049
+ }
3050
+ return null;
3051
+ }));
3052
+
3053
+ // Insert child items in the correct order
3054
+ let offset = 0;
3055
+ for (const result of expandedFolderResults) {
3056
+ if (result) {
3057
+ const {
3058
+ folderIndex,
3059
+ childItems
3060
+ } = result;
3061
+ newDirents.splice(folderIndex + 1 + offset, 0, ...childItems);
3062
+ offset += childItems.length;
3063
+ }
3064
+ }
2977
3065
  const maxLineY = getExplorerMaxLineY(minLineY, height, itemHeight, newDirents.length);
2978
3066
  const visible = newDirents.slice(minLineY, maxLineY);
2979
3067
  const {
2980
3068
  icons,
2981
3069
  newFileIconCache
2982
3070
  } = await getFileIcons(visible, fileIconCache);
2983
- const state3 = {
3071
+ let newFocusedIndex = focusedIndex;
3072
+ if (focusedIndex >= newDirents.length) {
3073
+ newFocusedIndex = newDirents.length - 1;
3074
+ }
3075
+ return {
2984
3076
  ...state,
2985
3077
  items: newDirents,
2986
3078
  fileIconCache: newFileIconCache,
2987
3079
  icons,
2988
- maxLineY
3080
+ maxLineY,
3081
+ focusedIndex: newFocusedIndex
2989
3082
  };
2990
- return state3;
2991
3083
  };
2992
3084
 
2993
3085
  const applyOperation = operation => {
@@ -3084,7 +3176,7 @@ const uploadFileSystemHandles = async (root, pathSeparator, fileSystemHandles) =
3084
3176
  return true;
3085
3177
  };
3086
3178
 
3087
- const mergeDirents$1 = (oldDirents, newDirents) => {
3179
+ const mergeDirents$2 = (oldDirents, newDirents) => {
3088
3180
  return newDirents;
3089
3181
  };
3090
3182
  const getMergedDirents$2 = async (root, pathSeparator, dirents) => {
@@ -3092,7 +3184,7 @@ const getMergedDirents$2 = async (root, pathSeparator, dirents) => {
3092
3184
  path: root,
3093
3185
  depth: 0
3094
3186
  });
3095
- const mergedDirents = mergeDirents$1(dirents, childDirents);
3187
+ const mergedDirents = mergeDirents$2(dirents, childDirents);
3096
3188
  return mergedDirents;
3097
3189
  };
3098
3190
  const handleDrop$2 = async (state, fileHandles, files) => {
@@ -3141,7 +3233,7 @@ const copyFilesElectron = async (root, pathSeparator, fileHandles, files, paths)
3141
3233
  await applyFileOperations(operations);
3142
3234
  };
3143
3235
 
3144
- const mergeDirents = (oldDirents, newDirents) => {
3236
+ const mergeDirents$1 = (oldDirents, newDirents) => {
3145
3237
  return newDirents;
3146
3238
  };
3147
3239
  const getMergedDirents$1 = async (root, pathSeparator, dirents) => {
@@ -3149,7 +3241,7 @@ const getMergedDirents$1 = async (root, pathSeparator, dirents) => {
3149
3241
  path: root,
3150
3242
  depth: 0
3151
3243
  });
3152
- const mergedDirents = mergeDirents(dirents, childDirents);
3244
+ const mergedDirents = mergeDirents$1(dirents, childDirents);
3153
3245
  return mergedDirents;
3154
3246
  };
3155
3247
  const handleDrop$1 = async (state, fileHandles, files, paths) => {
@@ -3335,6 +3427,92 @@ const handleInputClick = state => {
3335
3427
  return state;
3336
3428
  };
3337
3429
 
3430
+ const filterByFocusWord = (items, focusedIndex, focusWord) => {
3431
+ if (items.length === 0) {
3432
+ return -1;
3433
+ }
3434
+ const matches = [];
3435
+ for (let i = 0; i < items.length; i++) {
3436
+ if (items[i].toLowerCase().includes(focusWord)) {
3437
+ matches.push(i);
3438
+ }
3439
+ }
3440
+ if (matches.length === 0) {
3441
+ return -1;
3442
+ }
3443
+
3444
+ // Find the next match after the current focus
3445
+ let nextIndex = matches.findIndex(index => index > focusedIndex);
3446
+ if (nextIndex === -1) {
3447
+ // If no match found after current focus, wrap around to the first match
3448
+ nextIndex = 0;
3449
+ }
3450
+ return matches[nextIndex];
3451
+ };
3452
+
3453
+ const RE_ASCII = /^[a-z]$/;
3454
+ const isAscii = key => {
3455
+ return RE_ASCII.test(key);
3456
+ };
3457
+
3458
+ let timeout;
3459
+ const handleKeyDown = (state, key) => {
3460
+ const {
3461
+ focusWord,
3462
+ items,
3463
+ focusedIndex,
3464
+ focusWordTimeout
3465
+ } = state;
3466
+ if (focusWord && key === '') {
3467
+ return cancelTypeAhead(state);
3468
+ }
3469
+ if (!isAscii(key)) {
3470
+ return state;
3471
+ }
3472
+ const newFocusWord = focusWord + key.toLowerCase();
3473
+ const itemNames = items.map(item => item.name);
3474
+ const matchingIndex = filterByFocusWord(itemNames, focusedIndex, newFocusWord);
3475
+ if (timeout) {
3476
+ clearTimeout(timeout);
3477
+ }
3478
+
3479
+ // @ts-ignore
3480
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
3481
+ timeout = setTimeout(async () => {
3482
+ await invoke('Explorer.cancelTypeAhead');
3483
+ }, focusWordTimeout);
3484
+ if (matchingIndex === -1) {
3485
+ return {
3486
+ ...state,
3487
+ focusWord: newFocusWord
3488
+ };
3489
+ }
3490
+ return {
3491
+ ...state,
3492
+ focusWord: newFocusWord,
3493
+ focusedIndex: matchingIndex
3494
+ };
3495
+ };
3496
+
3497
+ const getTopLevelDirents = (root, pathSeparator, excluded) => {
3498
+ if (!root) {
3499
+ return [];
3500
+ }
3501
+ return getChildDirents(pathSeparator, {
3502
+ depth: 0,
3503
+ path: root,
3504
+ type: Directory
3505
+ }, excluded);
3506
+ };
3507
+
3508
+ const mergeDirents = (oldDirents, newDirents) => {
3509
+ const merged = [];
3510
+ for (const newDirent of newDirents) {
3511
+ merged.push(newDirent);
3512
+ }
3513
+ return merged;
3514
+ };
3515
+
3338
3516
  // TODO add lots of tests for this
3339
3517
  const updateRoot = async state1 => {
3340
3518
  // @ts-ignore
@@ -3349,7 +3527,7 @@ const updateRoot = async state1 => {
3349
3527
  // if (state2.disposed || state2.root !== state1.root) {
3350
3528
  // return state2
3351
3529
  // }
3352
- const newDirents = mergeDirents$2(state1.items, topLevelDirents);
3530
+ const newDirents = mergeDirents(state1.items, topLevelDirents);
3353
3531
  const state3 = {
3354
3532
  ...state1,
3355
3533
  items: newDirents
@@ -3837,70 +4015,35 @@ const openContainingFolder = async state => {
3837
4015
  return state;
3838
4016
  };
3839
4017
 
3840
- // TODO support multiselection and removing multiple dirents
3841
- const removeDirent = async state => {
3842
- if (state.focusedIndex < 0) {
3843
- return state;
3844
- }
3845
- const dirent = getFocusedDirent$1(state);
3846
- if (!dirent) {
3847
- return state;
3848
- }
3849
- const absolutePath = dirent.path;
3850
- try {
3851
- // TODO handle error
3852
- await remove(absolutePath);
3853
- } catch {
3854
- // TODO vscode shows error as alert (no stacktrace) and retry button
3855
- // maybe should show alert as well, but where to put stacktrace?
3856
- // on web should probably show notification (dialog)
3857
- // ErrorHandling.handleError(error)
3858
- // await ErrorHandling.showErrorDialog(error)
3859
- return state;
3860
- }
3861
- // TODO avoid state mutation
3862
- // @ts-ignore
3863
- const newVersion = ++state.version;
3864
- // TODO race condition
3865
- // const newState = await loadContent(state:any)
3866
- // @ts-ignore
3867
- if (state.version !== newVersion || state.disposed) {
3868
- return state;
3869
- }
3870
- // TODO is it possible to make this more functional instead of mutating state?
3871
- // maybe every function returns a new state?
3872
- const index = state.items.indexOf(dirent);
3873
- let deleteEnd = index + 1;
3874
- for (; deleteEnd < state.items.length; deleteEnd++) {
3875
- if (state.items[deleteEnd].depth <= dirent.depth) {
3876
- break;
4018
+ const getSelectedItems = (items, focusedIndex) => {
4019
+ const dirent = items[focusedIndex];
4020
+ const selectedItems = items.filter(item => item.selected || item === dirent);
4021
+ return selectedItems;
4022
+ };
4023
+
4024
+ const removePaths = async paths => {
4025
+ for (const item of paths) {
4026
+ try {
4027
+ await remove(item);
4028
+ } catch {
4029
+ // ignore
3877
4030
  }
3878
4031
  }
3879
- const deleteCount = deleteEnd - index;
3880
- const newDirents = [...state.items];
3881
- newDirents.splice(index, deleteCount);
3882
- let indexToFocus = -1;
3883
- if (newDirents.length === 0) {
3884
- indexToFocus = -1;
3885
- } else if (index < state.focusedIndex) {
3886
- indexToFocus = state.focusedIndex - 1;
3887
- } else if (index === state.focusedIndex) {
3888
- indexToFocus = Math.max(state.focusedIndex - 1, 0);
3889
- } else {
3890
- indexToFocus = Math.max(state.focusedIndex - 1, 0);
3891
- }
3892
- const visible = newDirents.slice(state.minLineY, state.maxLineY);
4032
+ };
4033
+
4034
+ const removeDirent = async state => {
3893
4035
  const {
3894
- icons,
3895
- newFileIconCache
3896
- } = await getFileIcons(visible, state.fileIconCache);
3897
- return {
3898
- ...state,
3899
- items: newDirents,
3900
- icons,
3901
- fileIconCache: newFileIconCache,
3902
- focusedIndex: indexToFocus
3903
- };
4036
+ items,
4037
+ focusedIndex
4038
+ } = state;
4039
+ const selectedItems = getSelectedItems(items, focusedIndex);
4040
+ if (selectedItems.length === 0) {
4041
+ return state;
4042
+ }
4043
+ const toRemove = getPaths(selectedItems);
4044
+ await removePaths(toRemove);
4045
+ const newState = await refresh(state);
4046
+ return newState;
3904
4047
  };
3905
4048
 
3906
4049
  const getNewDirentsForRename = (items, focusedIndex) => {
@@ -4030,6 +4173,7 @@ const HandleInputBlur = 'handleInputBlur';
4030
4173
  const HandleInputClick = 'handleInputClick';
4031
4174
  const HandleListBlur = 'handleListBlur';
4032
4175
  const HandleListFocus = 'handleListFocus';
4176
+ const HandleListKeyDown = 'handleListKeyDown';
4033
4177
  const HandlePointerDown = 'handlePointerDown';
4034
4178
  const HandleWheel = 'handleWheel';
4035
4179
 
@@ -4214,7 +4358,8 @@ const getExplorerVirtualDom = (visibleItems, focusedIndex, root, isWide, focused
4214
4358
  onDrop: HandleDrop,
4215
4359
  onFocus: HandleListFocus,
4216
4360
  onPointerDown: HandlePointerDown,
4217
- onWheel: HandleWheel
4361
+ onWheel: HandleWheel,
4362
+ onKeyDown: HandleListKeyDown
4218
4363
  }, ...visibleItems.flatMap(getExplorerItemVirtualDom)];
4219
4364
  return dom;
4220
4365
  };
@@ -4480,6 +4625,10 @@ const renderEventListeners = () => {
4480
4625
  return [{
4481
4626
  name: HandleInputBlur,
4482
4627
  params: ['handleInputBlur']
4628
+ }, {
4629
+ name: HandleListKeyDown,
4630
+ params: ['handleKeyDown', 'event.key'],
4631
+ preventDefault: true
4483
4632
  }, {
4484
4633
  name: HandleListFocus,
4485
4634
  params: ['handleFocus', 'event.isTrusted', 'event.target.className']
@@ -4880,6 +5029,8 @@ const updateEditingValue = async (state, value, inputSource = User) => {
4880
5029
 
4881
5030
  const commandMap = {
4882
5031
  'Explorer.acceptEdit': wrapCommand(acceptEdit),
5032
+ 'Explorer.handleKeyDown': wrapCommand(handleKeyDown),
5033
+ 'Explorer.cancelTypeAhead': wrapCommand(cancelTypeAhead),
4883
5034
  'Explorer.cancelEdit': wrapCommand(cancelEdit),
4884
5035
  'Explorer.collapseAll': wrapCommand(collapseAll),
4885
5036
  'Explorer.copyPath': wrapCommand(copyPath),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/explorer-view",
3
- "version": "2.28.0",
3
+ "version": "2.30.0",
4
4
  "description": "Explorer Worker",
5
5
  "repository": {
6
6
  "type": "git",