Browse Source

Closes #1190 adds order toggle to rebase editor

main
Keith Daulton 2 years ago
parent
commit
f55b2ad418
8 changed files with 219 additions and 26 deletions
  1. +2
    -0
      CHANGELOG.md
  2. +22
    -0
      package.json
  3. +3
    -0
      src/config.ts
  4. +15
    -0
      src/webviews/apps/rebase/rebase.html
  5. +73
    -5
      src/webviews/apps/rebase/rebase.scss
  6. +72
    -20
      src/webviews/apps/rebase/rebase.ts
  7. +7
    -0
      src/webviews/rebase/protocol.ts
  8. +25
    -1
      src/webviews/rebase/rebaseEditor.ts

+ 2
- 0
CHANGELOG.md View File

@ -14,6 +14,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Adds editor link highlighting to the _File Changes_ annotations for easier discovery of the added or changed lines
- Adds a `line` option to `gitlens.changes.locations` setting to specify whether to add a line highlight to the _File Changes_ annotations
- Adds "vanilla" [Gerrit](https://www.gerritcodereview.com/) remote provider support — closes [#1953](https://github.com/gitkraken/vscode-gitlens/issues/1953) thanks to [PR #1954](https://github.com/gitkraken/vscode-gitlens/pull/1954) by Felipe Santos ([@felipecrs](https://github.com/felipecrs))
- Adds "Oldest first" toggle to Interactive Rebase — closes [#1190](https://github.com/gitkraken/vscode-gitlens/issues/1190)
- Adds a `gitlens.rebaseEditor.ordering` setting to specify how Git commits are displayed in the _Interactive Rebase Editor_
## Changed

+ 22
- 0
package.json View File

@ -2065,6 +2065,28 @@
}
},
{
"id": "rebase-editor",
"title": "Interactive Rebase Editor",
"order": 105,
"properties": {
"gitlens.rebaseEditor.ordering": {
"type": "string",
"default": "desc",
"enum": [
"asc",
"desc"
],
"enumDescriptions": [
"Shows oldest commit first",
"Shows newest commit first"
],
"markdownDescription": "Specifies how Git commits are displayed in the _Interactive Rebase Editor_",
"scope": "window",
"order": 10
}
}
},
{
"id": "git-command-palette",
"title": "Git Command Palette",
"order": 110,

+ 3
- 0
src/config.ts View File

@ -121,6 +121,9 @@ export interface Config {
url: string | null;
strictSSL: boolean;
} | null;
rebaseEditor: {
ordering: 'asc' | 'desc';
};
remotes: RemotesConfig[] | null;
showWelcomeOnInstall: boolean;
showWhatsNewAfterUpgrades: boolean;

+ 15
- 0
src/webviews/apps/rebase/rebase.html View File

@ -9,6 +9,21 @@
<header>
<h2>GitLens Interactive Rebase</h2>
<h4 id="subhead"></h4>
<div class="toggle">
<input
class="toggle__input sr-only"
tabindex="1"
data-action="reorder"
id="ordering"
name="ordering"
type="checkbox"
/>
<span aria-hidden="true" class="toggle__indicator"></span>
<label class="toggle__label" for="ordering" aria-label="Change order to oldest first"
>Oldest first</label
>
<!-- should have an <div aria-live="polite" /> to notify order change -->
</div>
</header>
<ul id="entries" class="entries"></ul>
<div class="shortcuts">

+ 73
- 5
src/webviews/apps/rebase/rebase.scss View File

@ -208,8 +208,6 @@ $entry-padding: 5px;
&.entry--base,
&.entry--done {
margin-top: 5px;
& > .entry-action {
opacity: 0.8;
}
@ -249,8 +247,17 @@ $entry-padding: 5px;
}
}
&.entry--done {
margin-top: 5px;
}
&.entry--base {
margin-top: 10px;
.entries--ascending & {
margin-bottom: 10px;
}
.entries:not(.entries--ascending) & {
margin-top: 10px;
}
.vscode-dark & {
background: rgba(255, 255, 255, 0.1);
@ -263,8 +270,10 @@ $entry-padding: 5px;
}
}
.entries--base &:nth-last-of-type(2),
:not(.entries--base) &:nth-last-of-type(1) {
.entries--base:not(.entries--ascending) &:nth-last-of-type(2),
.entries:not(.entries--base):not(.entries--ascending) &:nth-last-of-type(1),
.entries--base.entries--ascending &:nth-of-type(2),
.entries--ascending:not(.entries--base) &:nth-of-type(1) {
& select {
& > option[value='squash'],
& > option[value='fixup'] {
@ -399,4 +408,63 @@ $entry-padding: 5px;
}
}
.toggle {
display: inline-flex;
flex-direction: row-reverse;
align-items: center;
gap: 0.5em;
&__input,
&__indicator {
width: 2.4em;
height: 1.4em;
}
&__input {
position: absolute;
appearance: none;
opacity: 0;
border-radius: 1em;
padding: 0;
&:focus {
border-radius: 1em;
}
}
&__indicator {
position: relative;
pointer-events: none;
display: block;
flex: none;
border-radius: 1em;
background-color: var(--color-background--lighten-075);
border: 1px solid var(--color-background--lighten-075);
&::before {
content: '';
top: 0.1em;
left: 0.1em;
position: absolute;
width: 1.2em;
height: 1.2em;
border-radius: 100%;
background-color: var(--color-button-foreground);
:checked ~ & {
transform: translateX(1em);
}
}
:checked ~ & {
background-color: var(--color-highlight);
}
:focus ~ & {
border-color: var(--color-background);
box-shadow: 0 0 0 1px var(--color-focus-border);
}
}
}
@import '../shared/codicons';

+ 72
- 20
src/webviews/apps/rebase/rebase.ts View File

@ -10,6 +10,7 @@ import {
MoveEntryCommandType,
RebaseEntry,
RebaseEntryAction,
ReorderCommandType,
StartCommandType,
State,
SwitchCommandType,
@ -61,7 +62,10 @@ class RebaseEditor extends App {
let squashing = false;
let squashToHere = false;
const $entries = document.querySelectorAll<HTMLLIElement>('li[data-ref]');
const $entries = [...document.querySelectorAll<HTMLLIElement>('li[data-ref]')];
if (this.state.ascending) {
$entries.reverse();
}
for (const $entry of $entries) {
squashToHere = false;
if ($entry.classList.contains('entry--squash') || $entry.classList.contains('entry--fixup')) {
@ -86,7 +90,11 @@ class RebaseEditor extends App {
const ref = e.item.dataset.ref;
if (ref != null) {
this.moveEntry(ref, e.newIndex, false);
let indexTarget = e.newIndex;
if (this.state.ascending && e.oldIndex) {
indexTarget = this.getEntryIndex(ref) + (indexTarget - e.oldIndex) * -1;
}
this.moveEntry(ref, indexTarget, false);
document.querySelectorAll<HTMLLIElement>(`li[data-ref="${ref}"]`)[0]?.focus();
}
@ -142,12 +150,17 @@ class RebaseEditor extends App {
}
} else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
if (!e.metaKey && !e.ctrlKey && !e.shiftKey) {
const advance =
(e.key === 'ArrowDown' && !this.state.ascending) ||
(e.key === 'ArrowUp' && this.state.ascending)
? 1
: -1;
if (e.altKey) {
const ref = target.dataset.ref;
if (ref) {
e.stopPropagation();
this.moveEntry(ref, e.key === 'ArrowDown' ? 1 : -1, true);
this.moveEntry(ref, advance, true);
}
} else {
if (this.state == null) return;
@ -157,7 +170,7 @@ class RebaseEditor extends App {
e.preventDefault();
let index = this.getEntryIndex(ref) + (e.key === 'ArrowDown' ? 1 : -1);
let index = this.getEntryIndex(ref) + advance;
if (index < 0) {
index = this.state.entries.length - 1;
} else if (index === this.state.entries.length) {
@ -210,6 +223,9 @@ class RebaseEditor extends App {
}
}),
DOM.on('select[data-ref]', 'input', (e, target: HTMLSelectElement) => this.onSelectChanged(target)),
DOM.on('input[data-action="reorder"]', 'input', (e, target: HTMLInputElement) =>
this.onOrderChanged(target),
),
);
return disposables;
@ -269,6 +285,14 @@ class RebaseEditor extends App {
this.sendCommand(SwitchCommandType, undefined);
}
private onOrderChanged($el: HTMLInputElement) {
const isChecked = $el.checked;
this.sendCommand(ReorderCommandType, {
ascending: isChecked,
});
}
protected override onMessageReceived(e: MessageEvent) {
const msg = e.data;
@ -342,20 +366,36 @@ class RebaseEditor extends App {
let squashToHere = false;
let tabIndex = 0;
for (const entry of state.entries) {
squashToHere = false;
if (entry.action === 'squash' || entry.action === 'fixup') {
squashing = true;
} else if (squashing) {
if (entry.action !== 'drop') {
squashToHere = true;
squashing = false;
const $entries = document.createDocumentFragment();
const appendEntries = () => {
const appendEntry = (entry: RebaseEntry) => {
squashToHere = false;
if (entry.action === 'squash' || entry.action === 'fixup') {
squashing = true;
} else if (squashing) {
if (entry.action !== 'drop') {
squashToHere = true;
squashing = false;
}
}
let $el: HTMLLIElement;
[$el, tabIndex] = this.createEntry(entry, state, ++tabIndex, squashToHere);
return $el;
};
const entryList = state.entries.map(appendEntry);
if (state.ascending) {
entryList.reverse().forEach($el => $entries.appendChild($el));
} else {
entryList.forEach($el => $entries.appendChild($el));
}
};
let $el: HTMLLIElement;
[$el, tabIndex] = this.createEntry(entry, state, ++tabIndex, squashToHere);
$container.appendChild($el);
if (!state.ascending) {
$container.classList.remove('entries--ascending');
appendEntries();
}
if (state.onto) {
@ -372,11 +412,23 @@ class RebaseEditor extends App {
++tabIndex,
false,
);
$container.appendChild($el);
$entries.appendChild($el);
$container.classList.add('entries--base');
}
}
if (state.ascending) {
$container.classList.add('entries--ascending');
appendEntries();
}
const $checkbox = document.getElementById('ordering');
if ($checkbox != null) {
($checkbox as HTMLInputElement).checked = state.ascending;
}
$container.appendChild($entries);
document
.querySelectorAll<HTMLLIElement>(
`${focusSelect ? 'select' : 'li'}[data-ref="${focusRef ?? state.entries[0].ref}"]`,
@ -398,7 +450,7 @@ class RebaseEditor extends App {
$entry.dataset.ref = entry.ref;
if (entry.action != null) {
$entry.tabIndex = tabIndex++;
$entry.tabIndex = 0;
const $dragHandle = document.createElement('span');
$dragHandle.classList.add('entry-handle');
@ -411,8 +463,8 @@ class RebaseEditor extends App {
const $select = document.createElement('select');
$select.dataset.ref = entry.ref;
$select.name = 'action';
$select.tabIndex = tabIndex++;
const $options = document.createDocumentFragment();
for (const action of rebaseActions) {
const $option = document.createElement('option');
$option.value = action;
@ -422,8 +474,9 @@ class RebaseEditor extends App {
$option.selected = true;
}
$select.appendChild($option);
$options.appendChild($option);
}
$select.appendChild($options);
$selectContainer.appendChild($select);
}
@ -465,7 +518,6 @@ class RebaseEditor extends App {
// $ref.dataset.prev = prev ? `${prev} \u2190 ` : '';
$ref.href = commit?.ref ? state.commands.commit.replace(this.commitTokenRegex, commit.ref) : '#';
$ref.textContent = entry.ref.substr(0, 7);
$ref.tabIndex = tabIndex++;
$entry.appendChild($ref);
return [$entry, tabIndex];

+ 7
- 0
src/webviews/rebase/protocol.ts View File

@ -10,6 +10,8 @@ export interface State {
commands: {
commit: string;
};
ascending: boolean;
}
export interface RebaseEntry {
@ -48,6 +50,11 @@ export const StartCommandType = new IpcCommandType('rebase/start');
export const SwitchCommandType = new IpcCommandType('rebase/switch');
export interface ReorderParams {
ascending: boolean;
}
export const ReorderCommandType = new IpcCommandType<ReorderParams>('rebase/reorder');
export interface ChangeEntryParams {
ref: string;
action: RebaseEntryAction;

+ 25
- 1
src/webviews/rebase/rebaseEditor.ts View File

@ -36,6 +36,8 @@ import {
MoveEntryCommandType,
RebaseEntry,
RebaseEntryAction,
ReorderCommandType,
ReorderParams,
StartCommandType,
State,
SwitchCommandType,
@ -98,6 +100,7 @@ interface RebaseEditorContext {
export class RebaseEditorProvider implements CustomTextEditorProvider, Disposable {
private readonly _disposable: Disposable;
private ascending = false;
constructor(private readonly container: Container) {
this._disposable = Disposable.from(
@ -108,6 +111,7 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
},
}),
);
this.ascending = configuration.get('rebaseEditor.ordering') === 'asc';
}
dispose() {
@ -245,7 +249,13 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
private async parseState(context: RebaseEditorContext): Promise<State> {
const branch = await this.container.git.getBranch(context.repoPath);
const state = await parseRebaseTodo(this.container, context.document.getText(), context.repoPath, branch?.name);
const state = await parseRebaseTodo(
this.container,
context.document.getText(),
context.repoPath,
branch?.name,
this.ascending,
);
return state;
}
@ -288,6 +298,12 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
onIpc(SwitchCommandType, e, () => this.switch(context));
break;
case ReorderCommandType.method:
onIpc(ReorderCommandType, e, params => {
this.reorder(params, context);
});
break;
case ChangeEntryCommandType.method:
onIpc(ChangeEntryCommandType, e, async params => {
const entries = parseRebaseTodoEntries(context.document);
@ -469,6 +485,12 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
});
}
private reorder(params: ReorderParams, context: RebaseEditorContext) {
this.ascending = params.ascending ?? false;
void configuration.updateEffective('rebaseEditor.ordering', this.ascending ? 'asc' : 'desc');
void this.getStateAndNotify(context);
}
private async getHtml(context: RebaseEditorContext): Promise<string> {
const webRootUri = Uri.joinPath(this.container.context.extensionUri, 'dist', 'webviews');
const uri = Uri.joinPath(webRootUri, 'rebase.html');
@ -516,6 +538,7 @@ async function parseRebaseTodo(
contents: string | { entries: RebaseEntry[]; onto: string },
repoPath: string,
branch: string | undefined,
ascending: boolean,
): Promise<Omit<State, 'rebasing'>> {
let onto: string;
let entries;
@ -598,6 +621,7 @@ async function parseRebaseTodo(
commands: {
commit: ShowQuickCommitCommand.getMarkdownCommandArgs(`\${commit}`, repoPath),
},
ascending: ascending,
};
}

Loading…
Cancel
Save