Skip to content

Commit 8578ba3

Browse files
authored
File selection improvements (#4134)
* Select multiple files with arrow keys + Shift There are some corner cases to cover, but it serves the basic usage * Refactor: remove unused code Looks like this code was used for the tree view structure * Check/uncheck files based on the file selection * Lint error fixes
1 parent 9f6823e commit 8578ba3

File tree

6 files changed

+144
-46
lines changed

6 files changed

+144
-46
lines changed

app/src/lib/components/BranchFilesList.svelte

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,15 @@
9292
}}
9393
on:keydown={(e) => {
9494
e.preventDefault();
95-
maybeMoveSelection(e.key, file, displayedFiles, fileIdSelection);
95+
maybeMoveSelection(
96+
allowMultiple,
97+
e.shiftKey,
98+
e.key,
99+
file,
100+
displayedFiles,
101+
$fileIdSelection,
102+
fileIdSelection
103+
);
96104
}}
97105
/>
98106
{/each}

app/src/lib/components/FileListItem.svelte

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,17 @@
2929
const selectedFiles = fileIdSelection.files;
3030
3131
let checked = false;
32-
let indeterminate = false;
3332
let draggableElt: HTMLDivElement;
3433
3534
$: if (file && $selectedOwnership) {
36-
const fileId = file.id;
37-
checked = file.hunks.every((hunk) => $selectedOwnership?.contains(fileId, hunk.id));
38-
const selectedCount = file.hunks.filter((hunk) =>
39-
$selectedOwnership?.contains(fileId, hunk.id)
40-
).length;
41-
indeterminate = selectedCount > 0 && file.hunks.length - selectedCount > 0;
35+
checked = file.hunks.every((hunk) => $selectedOwnership?.contains(file.id, hunk.id));
4236
}
4337
38+
$: if ($fileIdSelection && draggableElt)
39+
updateFocus(draggableElt, file, fileIdSelection, $commit?.id);
40+
41+
$: popupMenu = updateContextMenu();
42+
4443
function updateContextMenu() {
4544
if (popupMenu) unmount(popupMenu);
4645
return mount(FileContextMenu, {
@@ -49,11 +48,6 @@
4948
});
5049
}
5150
52-
$: if ($fileIdSelection && draggableElt)
53-
updateFocus(draggableElt, file, fileIdSelection, $commit?.id);
54-
55-
$: popupMenu = updateContextMenu();
56-
5751
onDestroy(() => {
5852
if (popupMenu) {
5953
unmount(popupMenu);
@@ -124,13 +118,36 @@
124118
<Checkbox
125119
small
126120
{checked}
127-
{indeterminate}
128121
on:change={(e) => {
122+
const isChecked = e.detail;
129123
selectedOwnership?.update((ownership) => {
130-
if (e.detail) file.hunks.forEach((h) => ownership.add(file.id, h));
131-
if (!e.detail) file.hunks.forEach((h) => ownership.remove(file.id, h.id));
124+
if (isChecked) {
125+
file.hunks.forEach((h) => ownership.add(file.id, h));
126+
} else {
127+
file.hunks.forEach((h) => ownership.remove(file.id, h.id));
128+
}
132129
return ownership;
133130
});
131+
132+
$selectedFiles.then((files) => {
133+
if (files.length > 0 && files.includes(file)) {
134+
if (isChecked) {
135+
files.forEach((f) => {
136+
selectedOwnership?.update((ownership) => {
137+
f.hunks.forEach((h) => ownership.add(f.id, h));
138+
return ownership;
139+
});
140+
});
141+
} else {
142+
files.forEach((f) => {
143+
selectedOwnership?.update((ownership) => {
144+
f.hunks.forEach((h) => ownership.remove(f.id, h.id));
145+
return ownership;
146+
});
147+
});
148+
}
149+
}
150+
});
134151
}}
135152
/>
136153
{/if}
@@ -176,14 +193,9 @@
176193
}
177194
178195
.draggable {
179-
/* cursor: grab; */
180-
181196
&:hover {
182197
& .draggable-handle {
183-
/* width: 10px; */
184-
/* width: 6px; */
185198
opacity: 1;
186-
/* transition-delay: 0.5s; */
187199
}
188200
}
189201
}
@@ -200,8 +212,6 @@
200212
transition:
201213
width var(--transition-fast),
202214
opacity var(--transition-fast);
203-
/* transition-delay: 0s; */
204-
/* background-color: rgb(184, 150, 201); */
205215
}
206216
207217
.info {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export function getSelectionDirection(firstFileIndex: number, lastFileIndex: number) {
2+
// detect the direction of the selection
3+
const selectionDirection = lastFileIndex < firstFileIndex ? 'down' : 'up';
4+
5+
return selectionDirection;
6+
}

app/src/lib/utils/selectFilesInList.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getSelectionDirection } from './getSelectionDirection';
12
import { stringifyFileKey, type FileIdSelection } from '$lib/vbranches/fileIdSelection';
23
import { get } from 'svelte/store';
34
import type { AnyCommit, AnyFile } from '$lib/vbranches/types';
@@ -26,8 +27,10 @@ export function selectFilesInList(
2627
);
2728

2829
// detect the direction of the selection
29-
const selectionDirection =
30-
initiallySelectedIndex < sortedFiles.findIndex((f) => f.id === file.id) ? 'down' : 'up';
30+
const selectionDirection = getSelectionDirection(
31+
initiallySelectedIndex,
32+
sortedFiles.findIndex((f) => f.id === file.id)
33+
);
3134

3235
const updatedSelection = sortedFiles.slice(
3336
Math.min(
@@ -42,9 +45,11 @@ export function selectFilesInList(
4245

4346
selectedFileIds = updatedSelection.map((f) => stringifyFileKey(f.id, commit?.id));
4447

48+
// if the selection is in the opposite direction, reverse the selection
4549
if (selectionDirection === 'down') {
4650
selectedFileIds = selectedFileIds.reverse();
4751
}
52+
4853
fileIdSelection.set(selectedFileIds);
4954
} else {
5055
// if only one file is selected and it is already selected, unselect it

app/src/lib/utils/selection.ts

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,19 @@
11
/**
22
* Shared helper functions for manipulating selected files with keyboard.
33
*/
4+
import { getSelectionDirection } from './getSelectionDirection';
5+
import { stringifyFileKey, unstringifyFileKey } from '$lib/vbranches/fileIdSelection';
46
import type { FileIdSelection } from '$lib/vbranches/fileIdSelection';
57
import type { AnyFile } from '$lib/vbranches/types';
68

7-
export function getNextFile(files: AnyFile[], current: string) {
8-
const fileIndex = files.findIndex((f) => f.id === current);
9-
if (fileIndex !== -1 && fileIndex + 1 < files.length) return files[fileIndex + 1];
9+
export function getNextFile(files: AnyFile[], currentId: string): AnyFile | undefined {
10+
const fileIndex = files.findIndex((f) => f.id === currentId);
11+
return fileIndex !== -1 && fileIndex + 1 < files.length ? files[fileIndex + 1] : undefined;
1012
}
1113

12-
export function getPreviousFile(files: AnyFile[], current: string) {
13-
const fileIndex = files.findIndex((f) => f.id === current);
14-
if (fileIndex > 0) return files[fileIndex - 1];
15-
}
16-
17-
export function getFileByKey(key: string, current: string, files: AnyFile[]): AnyFile | undefined {
18-
if (key === 'ArrowUp') {
19-
return getPreviousFile(files, current);
20-
} else if (key === 'ArrowDown') {
21-
return getNextFile(files, current);
22-
}
14+
export function getPreviousFile(files: AnyFile[], currentId: string): AnyFile | undefined {
15+
const fileIndex = files.findIndex((f) => f.id === currentId);
16+
return fileIndex > 0 ? files[fileIndex - 1] : undefined;
2317
}
2418

2519
/**
@@ -36,21 +30,92 @@ export function updateFocus(
3630
commitId?: string
3731
) {
3832
const selected = fileIdSelection.only();
39-
if (!selected) return;
40-
if (selected.fileId === file.id && selected.commitId === commitId) elt.focus();
33+
if (selected && selected.fileId === file.id && selected.commitId === commitId) {
34+
elt.focus();
35+
}
4136
}
4237

4338
export function maybeMoveSelection(
39+
allowMultiple: boolean,
40+
shiftKey: boolean,
4441
key: string,
4542
file: AnyFile,
4643
files: AnyFile[],
44+
selectedFileIds: string[],
4745
fileIdSelection: FileIdSelection
4846
) {
49-
if (key !== 'ArrowUp' && key !== 'ArrowDown') return;
47+
if (selectedFileIds.length === 0) return;
48+
49+
const firstFileId = unstringifyFileKey(selectedFileIds[0]);
50+
const lastFileId = unstringifyFileKey(selectedFileIds[selectedFileIds.length - 1]);
51+
let selectionDirection = getSelectionDirection(
52+
files.findIndex((f) => f.id === lastFileId),
53+
files.findIndex((f) => f.id === firstFileId)
54+
);
55+
56+
function getAndAddFile(
57+
getFileFunc: (files: AnyFile[], id: string) => AnyFile | undefined,
58+
id: string
59+
) {
60+
const file = getFileFunc(files, id);
61+
if (file) {
62+
// if file is already selected, do nothing
63+
if (selectedFileIds.includes(stringifyFileKey(file.id))) return;
64+
65+
fileIdSelection.add(file.id);
66+
}
67+
}
68+
69+
function getAndClearAndAddFile(
70+
getFileFunc: (files: AnyFile[], id: string) => AnyFile | undefined,
71+
id: string
72+
) {
73+
const file = getFileFunc(files, id);
74+
if (file) {
75+
fileIdSelection.clear();
76+
fileIdSelection.add(file.id);
77+
}
78+
}
79+
80+
switch (key) {
81+
case 'ArrowUp':
82+
if (shiftKey && allowMultiple) {
83+
// Handle case if only one file is selected
84+
// we should update the selection direction
85+
if (selectedFileIds.length === 1) {
86+
selectionDirection = 'up';
87+
} else if (selectionDirection === 'down') {
88+
fileIdSelection.remove(lastFileId);
89+
}
90+
getAndAddFile(getPreviousFile, lastFileId);
91+
} else {
92+
// Handle reset of selection
93+
if (selectedFileIds.length > 1) {
94+
getAndClearAndAddFile(getPreviousFile, lastFileId);
95+
} else {
96+
getAndClearAndAddFile(getPreviousFile, file.id);
97+
}
98+
}
99+
break;
50100

51-
const newSelection = getFileByKey(key, file.id, files);
52-
if (newSelection) {
53-
fileIdSelection.clear();
54-
fileIdSelection.add(newSelection.id);
101+
case 'ArrowDown':
102+
if (shiftKey && allowMultiple) {
103+
// Handle case if only one file is selected
104+
// we should update the selection direction
105+
if (selectedFileIds.length === 1) {
106+
selectionDirection = 'down';
107+
} else if (selectionDirection === 'up') {
108+
fileIdSelection.remove(lastFileId);
109+
}
110+
getAndAddFile(getNextFile, lastFileId);
111+
} else {
112+
// Handle reset of selection
113+
if (selectedFileIds.length > 1) {
114+
getAndClearAndAddFile(getNextFile, lastFileId);
115+
} else {
116+
getAndClearAndAddFile(getNextFile, file.id);
117+
}
118+
}
119+
break;
55120
}
56121
}

app/src/lib/vbranches/fileIdSelection.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ export function stringifyFileKey(fileId: string, commitId?: string) {
1212
return fileId + '|' + commitId;
1313
}
1414

15+
export function unstringifyFileKey(fileKeyString: string): string {
16+
return fileKeyString.split('|')[0];
17+
}
18+
1519
export function parseFileKey(fileKeyString: string): FileKey {
1620
const [fileId, commitId] = fileKeyString.split('|');
1721

0 commit comments

Comments
 (0)