Browse Source

Adds drag & drop to rebase editor

Adds better rebase action display
Adds constraints to avoid invalid rebase
main
Eric Amodio 4 years ago
parent
commit
f8ebcbd306
7 changed files with 353 additions and 63 deletions
  1. +2
    -0
      package.json
  2. +74
    -11
      src/webviews/apps/rebase/rebase.ts
  3. +154
    -28
      src/webviews/apps/scss/rebase.scss
  4. +6
    -0
      src/webviews/apps/shared/theme.ts
  5. +2
    -1
      src/webviews/protocol.ts
  6. +105
    -23
      src/webviews/rebaseEditor.ts
  7. +10
    -0
      yarn.lock

+ 2
- 0
package.json View File

@ -6199,6 +6199,7 @@
"dayjs": "1.8.30",
"iconv-lite": "0.6.2",
"lodash-es": "4.17.15",
"sortablejs": "1.10.2",
"vsls": "1.0.2532"
},
"devDependencies": {
@ -6206,6 +6207,7 @@
"@types/keytar": "4.4.2",
"@types/lodash-es": "4.17.3",
"@types/node": "12.12.51",
"@types/sortablejs": "1.10.5",
"@types/vscode": "1.47.0",
"@typescript-eslint/eslint-plugin": "3.7.0",
"@typescript-eslint/parser": "3.7.0",

+ 74
- 11
src/webviews/apps/rebase/rebase.ts View File

@ -1,5 +1,6 @@
'use strict';
/*global document*/
import Sortable from 'sortablejs';
import {
onIpcNotification,
RebaseDidAbortCommandType,
@ -52,6 +53,51 @@ class RebaseEditor extends App {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const me = this;
const $container = document.getElementById('entries')!;
Sortable.create($container, {
animation: 150,
handle: '.entry-handle',
filter: '.entry--base',
dragClass: 'entry--drag',
ghostClass: 'entry--dragging',
onChange: () => {
let squashing = false;
let squashToHere = false;
const $entries = document.querySelectorAll<HTMLLIElement>('li[data-ref]');
for (const $entry of $entries) {
squashToHere = false;
if ($entry.classList.contains('entry--squash') || $entry.classList.contains('entry--fixup')) {
squashing = true;
} else if (squashing) {
if (!$entry.classList.contains('entry--drop')) {
squashToHere = true;
squashing = false;
}
}
$entry.classList.toggle(
'entry--squash-to',
squashToHere && !$entry.classList.contains('entry--base'),
);
}
},
onEnd: e => {
if (e.newIndex == null || e.newIndex === e.oldIndex) {
return;
}
const ref = e.item.dataset.ref;
if (ref) {
console.log(ref, e.newIndex, e.oldIndex);
this.moveEntry(ref, e.newIndex, false);
document.querySelectorAll<HTMLLIElement>(`li[data-ref="${ref}"]`)[0]?.focus();
}
},
onMove: e => !e.related.classList.contains('entry--base'),
});
disposables.push(
DOM.on('[data-action="start"]', 'click', () => this.onStartClicked()),
DOM.on('[data-action="abort"]', 'click', () => this.onAbortClicked()),
@ -76,7 +122,7 @@ class RebaseEditor extends App {
if (ref) {
e.stopPropagation();
me.moveEntry(ref, e.key === 'ArrowDown');
me.moveEntry(ref, e.key === 'ArrowDown' ? 1 : -1, true);
}
} else {
if (me.state == null) return;
@ -94,7 +140,7 @@ class RebaseEditor extends App {
}
ref = me.state.entries[index].ref;
document.querySelectorAll<HTMLLIElement>(`li[data-ref="${ref}`)[0]?.focus();
document.querySelectorAll<HTMLLIElement>(`li[data-ref="${ref}"]`)[0]?.focus();
}
}
} else if (!e.metaKey && !e.altKey && !e.ctrlKey) {
@ -128,12 +174,13 @@ class RebaseEditor extends App {
return this.state?.entries.findIndex(e => e.ref === ref) ?? -1;
}
private moveEntry(ref: string, down: boolean) {
private moveEntry(ref: string, index: number, relative: boolean) {
const entry = this.getEntry(ref);
if (entry !== undefined) {
this.sendCommand(RebaseDidMoveEntryCommandType, {
ref: entry.ref,
down: !down,
to: index,
relative: relative,
});
}
}
@ -185,7 +232,7 @@ class RebaseEditor extends App {
const $subhead = document.getElementById('subhead')! as HTMLHeadingElement;
$subhead.innerHTML = `<span class="branch ml-1 mr-1">${state.branch}</span><span>Rebasing ${
state.entries.length
} commit${state.entries.length > 1 ? 's' : ''} onto <span class="commit">${state.onto}</span>`;
} commit${state.entries.length !== 1 ? 's' : ''} onto <span class="commit">${state.onto}</span>`;
const $container = document.getElementById('entries')!;
@ -198,17 +245,29 @@ class RebaseEditor extends App {
$container.innerHTML = '';
if (state.entries.length === 0) return;
let squashing = false;
let squashToHere = false;
let tabIndex = 0;
// let prev: string | undefined;
for (const entry of state.entries.reverse()) {
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;
}
}
let $el: HTMLLIElement;
[$el, tabIndex] = this.createEntry(entry, state, ++tabIndex);
$container.appendChild($el);
// if (entry.action !== 'drop') {
// prev = entry.ref;
// }
if (squashToHere) {
$el.classList.add('entry--squash-to');
}
$container.appendChild($el);
}
const commit = state.commits.find(c => c.ref.startsWith(state.onto));
@ -243,6 +302,10 @@ class RebaseEditor extends App {
if (entry.action != null) {
$entry.tabIndex = tabIndex++;
const $dragHandle = document.createElement('span');
$dragHandle.classList.add('entry-handle');
$entry.appendChild($dragHandle);
const $selectContainer = document.createElement('div');
$selectContainer.classList.add('entry-action', 'select-container');
$entry.appendChild($selectContainer);

+ 154
- 28
src/webviews/apps/scss/rebase.scss View File

@ -34,6 +34,17 @@ h4 {
.entries {
grid-area: entries;
// border-left: 2px solid var(--color-highlight);
border-left: 2px solid;
margin-left: 10px;
padding-left: 4px;
.vscode-dark & {
border-color: var(--color-background--lighten-15);
}
.vscode-light & {
border-color: var(--color-background--darken-15);
}
}
.shortcuts {
@ -52,12 +63,32 @@ h4 {
justify-content: space-between;
margin: 0 10px;
padding: 10px 0;
border: 2px solid transparent;
border-radius: 3px;
&::after {
display: inline-block;
content: ' ';
background-color: var(--color-background);
// border: 2px solid var(--color-highlight);
border: 2px solid var(--color-foreground--50);
border-radius: 50%;
height: 12px;
width: 12px;
margin-left: -25px;
position: absolute;
z-index: 2;
}
&:focus-within {
outline: -webkit-focus-ring-color auto 1px;
outline: none;
border: 2px solid var(--color-highlight--50);
border-radius: 3px;
}
&.entry--base {
margin-left: 10px;
.vscode-dark & {
background: rgba(255, 255, 255, 0.1);
}
@ -66,11 +97,125 @@ h4 {
background: rgba(0, 0, 0, 0.1);
}
// &::after {
// background-color: var(--color-highlight);
// }
&:focus,
&:focus-within {
outline: none !important;
}
}
&.entry--squash-to {
&::after {
border: 2px solid rgba(212, 153, 0, 1);
z-index: 3;
}
&::before {
display: inline-block;
content: ' ';
background-color: var(--color-background);
border-left: 2px solid rgba(212, 153, 0, 1);
height: 32px;
width: 2px;
margin-left: -18px;
margin-top: -10px;
position: absolute;
z-index: 1;
}
}
&.entry--edit,
&.entry--reword {
&::after {
border: 2px solid rgba(0, 153, 0, 1);
z-index: 3;
}
}
&.entry--squash,
&.entry--fixup {
&::after {
display: none;
}
&::before {
display: inline-block;
content: ' ';
background-color: var(--color-background);
border-left: 2px solid rgba(212, 153, 0, 1);
height: 64px;
width: 2px;
margin-left: -18px;
margin-top: 6px;
position: absolute;
z-index: 1;
}
}
&.entry--drop {
&::after {
display: none;
}
&::before {
display: inline-block;
content: ' ';
background-color: var(--color-background);
border-left: 2px solid rgba(153, 0, 0, 1);
height: 48px;
width: 2px;
margin-left: -18px;
position: absolute;
z-index: 1;
}
}
&:nth-last-child(2) {
& select {
& > option[value='squash'],
& > option[value='fixup'] {
display: none;
}
}
&.entry--squash-to {
& select {
& > option[value='drop'] {
display: none;
}
}
}
}
}
.entry--drag {
opacity: 0 !important;
}
.entry--dragging {
background: var(--color-highlight--25);
opacity: 0.8;
}
.entry-handle {
display: inline-block;
border-left: 2px dotted;
border-right: 2px dotted;
height: 14px;
width: 3px;
margin-left: 10px;
cursor: ns-resize;
.vscode-dark & {
border-color: var(--color-foreground--75);
}
.vscode-light & {
border-color: var(--color-foreground--75);
}
}
.entry-action {
@ -109,32 +254,6 @@ h4 {
text-overflow: ellipsis;
white-space: nowrap;
.entry--squash &,
.entry--fixup & {
padding-left: 28px;
&::before {
content: ' ';
position: absolute;
top: 10px;
left: 6px;
width: 15px;
height: 18px;
border-top: 1px solid currentColor;
border-left: 1px solid currentColor;
}
&::after {
content: ' ';
position: absolute;
top: 14px;
left: 1px;
padding: 6px;
box-shadow: 1px -1px currentColor;
transform: rotate(135deg);
}
}
.entry--drop & {
text-decoration: line-through;
opacity: 0.25;
@ -145,8 +264,9 @@ h4 {
flex: auto 0 0;
margin: 0 -5px 0 0;
.entry--squash &,
.entry--fixup &,
.entry--drop & {
// text-decoration: line-through;
opacity: 0.25;
}
}
@ -158,6 +278,12 @@ h4 {
margin: 0 10px;
opacity: 0.5;
.vscode-light & {
opacity: 0.6;
}
.entry--squash &,
.entry--fixup &,
.entry--drop & {
text-decoration: line-through;
opacity: 0.25;

+ 6
- 0
src/webviews/apps/shared/theme.ts View File

@ -43,6 +43,12 @@ export function initializeAndWatchThemeColors() {
bodyStyle.setProperty('--color-button-background', color);
bodyStyle.setProperty('--color-button-background--darken-30', darken(color, 30));
color = computedStyle.getPropertyValue('--vscode-button-background').trim();
bodyStyle.setProperty('--color-highlight', color);
bodyStyle.setProperty('--color-highlight--75', opacity(color, 75));
bodyStyle.setProperty('--color-highlight--50', opacity(color, 50));
bodyStyle.setProperty('--color-highlight--25', opacity(color, 25));
color = computedStyle.getPropertyValue('--vscode-button-foreground').trim();
bodyStyle.setProperty('--color-button-foreground', color);

+ 2
- 1
src/webviews/protocol.ts View File

@ -119,7 +119,8 @@ export const RebaseDidChangeEntryCommandType = new IpcCommandType
export interface RebaseDidMoveEntryCommandParams {
ref: string;
down: boolean;
to: number;
relative: boolean;
}
export const RebaseDidMoveEntryCommandType = new IpcCommandType<RebaseDidMoveEntryCommandParams>('rebase/move/entry');

+ 105
- 23
src/webviews/rebaseEditor.ts View File

@ -1,6 +1,5 @@
'use strict';
import * as paths from 'path';
import * as fs from 'fs';
import { TextDecoder } from 'util';
import {
CancellationToken,
commands,
@ -128,7 +127,7 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
});
} while (true);
return entries;
return entries.reverse();
}
private parseEntriesAndSendChange(panel: WebviewPanel, document: TextDocument) {
@ -141,7 +140,7 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
}
private async parseState(document: TextDocument): Promise<RebaseState> {
const repoPath = await Container.git.getRepoPath(paths.join(document.uri.fsPath, '../../..'));
const repoPath = await Container.git.getRepoPath(Uri.joinPath(document.uri, '../../..'));
const branch = await Container.git.getBranch(repoPath);
const contents = document.getText();
@ -263,8 +262,48 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
new Range(new Position(start.line, 0), new Position(start.line, Number.MAX_SAFE_INTEGER)),
);
// Fake the new set of entries, so we can ensure that the last entry isn't a squash/fixup
const newEntries = [...entries];
newEntries.splice(entries.indexOf(entry), 1, {
...entry,
action: params.action,
});
let squashing = false;
for (const entry of newEntries) {
if (entry.action === 'squash' || entry.action === 'fixup') {
squashing = true;
} else if (squashing) {
if (entry.action !== 'drop') {
squashing = false;
}
}
}
const edit = new WorkspaceEdit();
edit.replace(document.uri, range, `${params.action} ${entry.ref} ${entry.message}`);
let action = params.action;
// Ensure that the last entry isn't a squash/fixup
if (squashing) {
const lastEntry = newEntries[newEntries.length - 1];
if (entry.ref === lastEntry.ref) {
action = 'pick';
} else {
const start = document.positionAt(lastEntry.index);
const range = document.validateRange(
new Range(
new Position(start.line, 0),
new Position(start.line, Number.MAX_SAFE_INTEGER),
),
);
edit.replace(document.uri, range, `pick ${lastEntry.ref} ${lastEntry.message}`);
}
}
edit.replace(document.uri, range, `${action} ${entry.ref} ${entry.message}`);
await workspace.applyEdit(edit);
});
@ -278,8 +317,24 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
if (entry == null) return;
const index = entries.findIndex(e => e.ref === params.ref);
if ((!params.down && index === 0) || (params.down && index === entries.length - 1)) {
return;
let newIndex;
if (params.relative) {
if ((params.to === -1 && index === 0) || (params.to === 1 && index === entries.length - 1)) {
return;
}
newIndex = index + params.to;
} else {
if (index === params.to) return;
newIndex = params.to;
}
const newEntry = entries[newIndex];
let newLine = document.positionAt(newEntry.index).line;
if (newIndex < index) {
newLine++;
}
const start = document.positionAt(entry.index);
@ -287,13 +342,48 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
new Range(new Position(start.line, 0), new Position(start.line + 1, 0)),
);
// Fake the new set of entries, so we can ensure that the last entry isn't a squash/fixup
const newEntries = [...entries];
newEntries.splice(index, 1);
newEntries.splice(newIndex, 0, entry);
let squashing = false;
for (const entry of newEntries) {
if (entry.action === 'squash' || entry.action === 'fixup') {
squashing = true;
} else if (squashing) {
if (entry.action !== 'drop') {
squashing = false;
}
}
}
const edit = new WorkspaceEdit();
let action = entry.action;
// Ensure that the last entry isn't a squash/fixup
if (squashing) {
const lastEntry = newEntries[newEntries.length - 1];
if (entry.ref === lastEntry.ref) {
action = 'pick';
} else {
const start = document.positionAt(lastEntry.index);
const range = document.validateRange(
new Range(
new Position(start.line, 0),
new Position(start.line, Number.MAX_SAFE_INTEGER),
),
);
edit.replace(document.uri, range, `pick ${lastEntry.ref} ${lastEntry.message}`);
}
}
edit.delete(document.uri, range);
edit.insert(
document.uri,
new Position(range.start.line + (params.down ? 2 : -1), 0),
`${entry.action} ${entry.ref} ${entry.message}\n`,
);
edit.insert(document.uri, new Position(newLine, 0), `${action} ${entry.ref} ${entry.message}\n`);
await workspace.applyEdit(edit);
});
@ -303,24 +393,16 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
private _html: string | undefined;
private async getHtml(webview: Webview, document: TextDocument): Promise<string> {
const filename = Container.context.asAbsolutePath(paths.join('dist/webviews/', 'rebase.html'));
const uri = Uri.joinPath(Container.context.extensionUri, 'dist', 'webviews', 'rebase.html');
let content;
// When we are debugging avoid any caching so that we can change the html and have it update without reloading
if (Logger.isDebugging) {
content = await new Promise<string>((resolve, reject) => {
fs.readFile(filename, 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
content = new TextDecoder('utf8').decode(await workspace.fs.readFile(uri));
} else {
if (this._html !== undefined) return this._html;
const doc = await workspace.openTextDocument(filename);
const doc = await workspace.openTextDocument(uri);
content = doc.getText();
}

+ 10
- 0
yarn.lock View File

@ -230,6 +230,11 @@
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
"@types/sortablejs@1.10.5":
version "1.10.5"
resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.10.5.tgz#341f5af3372180ce3cbfbd2b34a6fdefaca8d227"
integrity sha512-U/i9rGkCwmgmsa0UzHlGnQtL4y57tjywjp6j3GbA64MLaGO+sEBLVhBrQokWfgzeV2T6hQRYC4dZtXkl6dCMfw==
"@types/source-list-map@*":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
@ -6412,6 +6417,11 @@ sort-keys@^2.0.0:
dependencies:
is-plain-obj "^1.0.0"
sortablejs@1.10.2:
version "1.10.2"
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.2.tgz#6e40364d913f98b85a14f6678f92b5c1221f5290"
integrity sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==
source-list-map@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"

Loading…
Cancel
Save