BranchFilesList keybindings: Stage and unstage files

Stage or unstage the files by pressing the space-bar on the selected files.
Click enter to focus on the commit message input field
This commit is contained in:
estib 2024-09-11 17:20:56 +02:00
parent f3b3649bca
commit d41827c9d1
5 changed files with 123 additions and 34 deletions

View File

@ -52,6 +52,7 @@
let laneWidth: number | undefined = $state();
let commitDialog = $state<CommitDialog>();
let scrollViewport: HTMLElement | undefined = $state();
let rsViewport: HTMLElement | undefined = $state();
@ -131,6 +132,7 @@
showCheckboxes={$commitBoxOpen}
allowMultiple
commitDialogExpanded={commitBoxOpen}
focusCommitDialog={() => commitDialog?.focus()}
/>
{#if branch.conflicted}
<div class="card-notifications">
@ -149,6 +151,7 @@
</Dropzones>
<CommitDialog
bind:this={commitDialog}
projectId={project.id}
expanded={commitBoxOpen}
hasSectionsAfter={branch.commits.length > 0}

View File

@ -45,6 +45,10 @@
function close() {
$expanded = false;
}
export function focus() {
commitMessageInput.focus();
}
</script>
<div
@ -77,14 +81,7 @@
<div class="actions" class:commit-box__actions-expanded={$expanded}>
{#if $expanded && !isCommitting}
<div class="cancel-btn-wrapper" transition:slideFade={{ duration: 200, axis: 'x' }}>
<Button
style="ghost"
outline
id="commit-to-branch"
onclick={close}
>
Cancel
</Button>
<Button style="ghost" outline id="commit-to-branch" onclick={close}>Cancel</Button>
</div>
{/if}
<Button

View File

@ -10,6 +10,7 @@
export let isUnapplied: boolean;
export let showCheckboxes = false;
export let commitDialogExpanded: Writable<boolean>;
export let focusCommitDialog: () => void;
export let allowMultiple = false;
export let readonly = false;
@ -27,6 +28,7 @@
{showCheckboxes}
{isUnapplied}
{commitDialogExpanded}
{focusCommitDialog}
/>
{/if}
</div>

View File

@ -5,13 +5,14 @@
import TextBox from '$lib/shared/TextBox.svelte';
import { chunk } from '$lib/utils/array';
import { copyToClipboard } from '$lib/utils/clipboard';
import { getContext } from '$lib/utils/context';
import { getContext, maybeGetContextStore } from '$lib/utils/context';
import { KeyName } from '$lib/utils/hotkeys';
import { selectFilesInList } from '$lib/utils/selectFilesInList';
import { updateSelection } from '$lib/utils/selection';
import { getCommitStore } from '$lib/vbranches/contexts';
import { FileIdSelection, stringifyFileKey } from '$lib/vbranches/fileIdSelection';
import { sortLikeFileTree } from '$lib/vbranches/filetree';
import { SelectedOwnership, updateOwnership } from '$lib/vbranches/ownership';
import Button from '@gitbutler/ui/Button.svelte';
import type { AnyFile } from '$lib/vbranches/types';
import type { Writable } from 'svelte/store';
@ -24,7 +25,8 @@
showCheckboxes?: boolean;
allowMultiple?: boolean;
readonly?: boolean;
commitDialogExpanded: Writable<boolean> | undefined;
commitDialogExpanded?: Writable<boolean>;
focusCommitDialog?: () => void;
}
const {
@ -33,20 +35,65 @@
showCheckboxes = false,
allowMultiple = false,
readonly = false,
commitDialogExpanded = undefined
commitDialogExpanded,
focusCommitDialog
}: Props = $props();
const fileIdSelection = getContext(FileIdSelection);
const selectedOwnership: Writable<SelectedOwnership> | undefined =
maybeGetContextStore(SelectedOwnership);
const commit = getCommitStore();
let chunkedFiles: AnyFile[][] = $derived(chunk(sortLikeFileTree(files), 100));
let currentDisplayIndex = $state(0);
let displayedFiles: AnyFile[] = $derived(chunkedFiles.slice(0, currentDisplayIndex + 1).flat());
function startCommit() {
function handleSpace() {
if (commitDialogExpanded === undefined) return;
// Start commit
if (!$commitDialogExpanded) {
$commitDialogExpanded = true;
return;
}
// Stage/unstage files
updateOwnership({
selectedFileIds: $fileIdSelection,
files: displayedFiles,
selectedOwnership
});
}
function handleEnter() {
if (commitDialogExpanded === undefined || focusCommitDialog === undefined) return;
if ($commitDialogExpanded) {
focusCommitDialog();
}
}
function handleKeyDown(e: KeyboardEvent) {
e.preventDefault();
updateSelection({
allowMultiple,
shiftKey: e.shiftKey,
key: e.key,
targetElement: e.currentTarget as HTMLElement,
files: displayedFiles,
selectedFileIds: $fileIdSelection,
fileIdSelection,
commitId: $commit?.id
});
switch (e.key) {
case KeyName.Space: {
handleSpace();
break;
}
case KeyName.Enter: {
handleEnter();
break;
}
}
}
@ -86,28 +133,7 @@
loadMore();
}}
role="listbox"
onkeydown={(e) => {
e.preventDefault();
updateSelection(
{
allowMultiple,
shiftKey: e.shiftKey,
key: e.key,
targetElement: e.currentTarget as HTMLElement,
files: displayedFiles,
selectedFileIds: $fileIdSelection,
fileIdSelection,
commitId: $commit?.id
}
);
switch (e.key) {
case KeyName.Space: {
e.preventDefault();
startCommit();
break;
}
}
}}
onkeydown={handleKeyDown}
>
{#each displayedFiles as file (file.id)}
<FileListItem

View File

@ -1,4 +1,6 @@
import { unstringifyFileKey } from './fileIdSelection';
import type { VirtualBranch, AnyFile, Hunk, RemoteHunk, RemoteFile } from './types';
import type { Writable } from 'svelte/store';
export function filesToOwnership(files: AnyFile[]) {
return files
@ -203,3 +205,62 @@ export class SelectedOwnership {
return this.selection.size === 0;
}
}
interface OwnershipUpdateParams {
selectedFileIds: string[];
files: AnyFile[];
selectedOwnership: Writable<SelectedOwnership> | undefined;
}
export function updateOwnership(params: OwnershipUpdateParams) {
const selectedFiles = params.selectedFileIds
.map((id) => unstringifyFileKey(id))
.map((id) => params.files.find((f) => f.id === id))
.filter((f): f is AnyFile => !!f);
if (selectedFiles.length === 0) return;
// Only one file selected
//
// Select all hunks if none are selected,
// otherwise ignore all hunks
if (selectedFiles.length === 1) {
const file = selectedFiles[0]!;
params.selectedOwnership?.update((ownership) => {
const someHunksSelected = file.hunks.some((h) => ownership.isSelected(file.id, h.id));
if (someHunksSelected) {
ownership.ignore(file.id, ...file.hunkIds);
} else {
ownership.select(file.id, ...file.hunks);
}
return ownership;
});
return;
}
// Multiple files selected
//
// Select all files if none are selected,
// otherwise ignore all files
params.selectedOwnership?.update((ownership) => {
const someFilesSelected = selectedFiles.some((f) =>
f.hunks.some((h) => ownership.isSelected(f.id, h.id))
);
if (someFilesSelected) {
for (const file of selectedFiles) {
ownership.ignore(file.id, ...file.hunkIds);
}
} else {
for (const file of selectedFiles) {
ownership.select(file.id, ...file.hunks);
}
}
return ownership;
});
}