diff --git a/package.json b/package.json
index 578597d..155a827 100644
--- a/package.json
+++ b/package.json
@@ -6028,6 +6028,17 @@
"when": "gitlens:enabled && focusedView =~ /^gitlens\\.views\\.search/"
}
],
+ "customEditors": [
+ {
+ "viewType": "gitlens.rebase",
+ "displayName": "Interactive Rebase Editor",
+ "selector": [
+ {
+ "filenamePattern": "git-rebase-todo"
+ }
+ ]
+ }
+ ],
"resourceLabelFormatters": [
{
"scheme": "gitlens",
diff --git a/src/container.ts b/src/container.ts
index 59e6a44..b3c74c1 100644
--- a/src/container.ts
+++ b/src/container.ts
@@ -23,6 +23,7 @@ import { RepositoriesView } from './views/repositoriesView';
import { SearchView } from './views/searchView';
import { ViewCommands } from './views/viewCommands';
import { VslsController } from './vsls/vsls';
+import { RebaseEditorProvider } from './webviews/rebaseEditor';
import { SettingsWebview } from './webviews/settingsWebview';
import { WelcomeWebview } from './webviews/welcomeWebview';
@@ -109,6 +110,7 @@ export class Container {
});
}
+ context.subscriptions.push(new RebaseEditorProvider());
context.subscriptions.push(new GitFileSystemProvider());
context.subscriptions.push(configuration.onWillChange(this.onConfigurationChanging, this));
diff --git a/src/webviews/apps/rebase/rebase.html b/src/webviews/apps/rebase/rebase.html
new file mode 100644
index 0000000..0a42c3b
--- /dev/null
+++ b/src/webviews/apps/rebase/rebase.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+ pPick
+ rReword
+ eEdit
+ sSquash
+ dDrop
+ alt ↑Move Up
+ alt ↓Move Down
+
+
+
+
+
+
+ #{endOfBody}
+
+
diff --git a/src/webviews/apps/rebase/rebase.ts b/src/webviews/apps/rebase/rebase.ts
new file mode 100644
index 0000000..066ef32
--- /dev/null
+++ b/src/webviews/apps/rebase/rebase.ts
@@ -0,0 +1,314 @@
+'use strict';
+/*global document*/
+import {
+ onIpcNotification,
+ RebaseDidAbortCommandType,
+ RebaseDidChangeEntryCommandType,
+ RebaseDidChangeNotificationType,
+ RebaseDidMoveEntryCommandType,
+ RebaseDidStartCommandType,
+ RebaseEntry,
+ RebaseEntryAction,
+ RebaseState,
+} from '../../protocol';
+import { App } from '../shared/appBase';
+import { DOM } from '../shared/dom';
+
+const rebaseActions = ['pick', 'reword', 'edit', 'squash', 'fixup', 'drop'];
+const rebaseActionsMap = new Map([
+ ['p', 'pick'],
+ ['P', 'pick'],
+ ['r', 'reword'],
+ ['R', 'reword'],
+ ['e', 'edit'],
+ ['E', 'edit'],
+ ['s', 'squash'],
+ ['S', 'squash'],
+ ['f', 'fixup'],
+ ['F', 'fixup'],
+ ['d', 'drop'],
+ ['D', 'drop'],
+]);
+
+class RebaseEditor extends App {
+ // eslint-disable-next-line no-template-curly-in-string
+ private readonly commitTokenRegex = new RegExp(encodeURIComponent('${commit}'));
+
+ constructor() {
+ super('RebaseEditor', (window as any).bootstrap);
+ (window as any).bootstrap = undefined;
+ }
+
+ protected onInitialize() {
+ this.state = this.getState() ?? this.state;
+ if (this.state != null) {
+ this.refresh(this.state);
+ }
+ }
+
+ protected onBind() {
+ const disposables = super.onBind?.() ?? [];
+
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const me = this;
+
+ disposables.push(
+ DOM.on('[data-action="start"]', 'click', () => this.onStartClicked()),
+ DOM.on('[data-action="abort"]', 'click', () => this.onAbortClicked()),
+ DOM.on('li[data-ref]', 'keydown', function (this: Element, e: KeyboardEvent) {
+ if ((e.target as HTMLElement).matches('select[data-ref]')) {
+ if (e.key === 'Escape') {
+ (this as HTMLLIElement).focus();
+ }
+
+ return;
+ }
+
+ if (e.key === 'Enter' || e.key === ' ') {
+ const $select = (this as HTMLLIElement).querySelectorAll('select[data-ref]')[0];
+ if ($select != null) {
+ $select.focus();
+ }
+ } else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
+ if (!e.metaKey && !e.ctrlKey && !e.shiftKey) {
+ if (e.altKey) {
+ const ref = (this as HTMLLIElement).dataset.ref;
+ if (ref) {
+ e.stopPropagation();
+
+ me.moveEntry(ref, e.key === 'ArrowDown');
+ }
+ } else {
+ if (me.state == null) return;
+
+ let ref = (this as HTMLLIElement).dataset.ref;
+ if (ref == null) return;
+
+ e.preventDefault();
+
+ let index = me.getEntryIndex(ref) + (e.key === 'ArrowDown' ? 1 : -1);
+ if (index < 0) {
+ index = me.state.entries.length - 1;
+ } else if (index === me.state.entries.length) {
+ index = 0;
+ }
+
+ ref = me.state.entries[index].ref;
+ document.querySelectorAll(`li[data-ref="${ref}`)[0]?.focus();
+ }
+ }
+ } else if (!e.metaKey && !e.altKey && !e.ctrlKey) {
+ const action = rebaseActionsMap.get(e.key);
+ if (action !== undefined) {
+ e.stopPropagation();
+
+ const $select = (this as HTMLLIElement).querySelectorAll(
+ 'select[data-ref]',
+ )[0];
+ if ($select != null) {
+ $select.value = action;
+ me.onSelectChanged($select);
+ }
+ }
+ }
+ }),
+ DOM.on('select[data-ref]', 'input', function (this: Element) {
+ return me.onSelectChanged(this as HTMLSelectElement);
+ }),
+ );
+
+ return disposables;
+ }
+
+ private getEntry(ref: string) {
+ return this.state?.entries.find(e => e.ref === ref);
+ }
+
+ private getEntryIndex(ref: string) {
+ return this.state?.entries.findIndex(e => e.ref === ref) ?? -1;
+ }
+
+ private moveEntry(ref: string, down: boolean) {
+ const entry = this.getEntry(ref);
+ if (entry !== undefined) {
+ this.sendCommand(RebaseDidMoveEntryCommandType, {
+ ref: entry.ref,
+ down: !down,
+ });
+ }
+ }
+
+ private setEntryAction(ref: string, action: RebaseEntryAction) {
+ const entry = this.getEntry(ref);
+ if (entry !== undefined) {
+ if (entry.action === action) return;
+
+ this.sendCommand(RebaseDidChangeEntryCommandType, {
+ ref: entry.ref,
+ action: action,
+ });
+ }
+ }
+
+ private onAbortClicked() {
+ this.sendCommand(RebaseDidAbortCommandType, {});
+ }
+
+ private onSelectChanged($el: HTMLSelectElement) {
+ const ref = $el.dataset.ref;
+ if (ref) {
+ this.setEntryAction(ref, $el.options[$el.selectedIndex].value as RebaseEntryAction);
+ }
+ }
+
+ private onStartClicked() {
+ this.sendCommand(RebaseDidStartCommandType, {});
+ }
+
+ protected onMessageReceived(e: MessageEvent) {
+ const msg = e.data;
+
+ switch (msg.method) {
+ case RebaseDidChangeNotificationType.method:
+ onIpcNotification(RebaseDidChangeNotificationType, msg, params => {
+ this.setState({ ...this.state, ...params });
+ this.refresh(this.state);
+ });
+ break;
+
+ default:
+ super.onMessageReceived?.(e);
+ }
+ }
+
+ private refresh(state: RebaseState) {
+ const $subhead = document.getElementById('subhead')! as HTMLHeadingElement;
+ $subhead.innerHTML = `${state.branch}Rebasing ${
+ state.entries.length
+ } commit${state.entries.length > 1 ? 's' : ''} onto ${state.onto}`;
+
+ const $container = document.getElementById('entries')!;
+
+ const focusRef = document.activeElement?.closest('li[data-ref]')?.dataset.ref;
+ let focusSelect = false;
+ if (document.activeElement?.matches('select[data-ref]')) {
+ focusSelect = true;
+ }
+
+ $container.innerHTML = '';
+ if (state.entries.length === 0) return;
+
+ let tabIndex = 0;
+
+ // let prev: string | undefined;
+ for (const entry of state.entries.reverse()) {
+ let $el: HTMLLIElement;
+ [$el, tabIndex] = this.createEntry(entry, state, ++tabIndex);
+ $container.appendChild($el);
+
+ // if (entry.action !== 'drop') {
+ // prev = entry.ref;
+ // }
+ }
+
+ const commit = state.commits.find(c => c.ref.startsWith(state.onto));
+ if (commit) {
+ const [$el] = this.createEntry(
+ {
+ action: undefined!,
+ index: 0,
+ message: commit.message.split('\n')[0],
+ ref: state.onto,
+ },
+ state,
+ ++tabIndex,
+ );
+ $container.appendChild($el);
+ }
+
+ document
+ .querySelectorAll(
+ `${focusSelect ? 'select' : 'li'}[data-ref="${focusRef ?? state.entries[0].ref}"]`,
+ )[0]
+ ?.focus();
+
+ this.bind();
+ }
+
+ private createEntry(entry: RebaseEntry, state: RebaseState, tabIndex: number): [HTMLLIElement, number] {
+ const $entry = document.createElement('li');
+ $entry.classList.add('entry', `entry--${entry.action ?? 'base'}`);
+ $entry.dataset.ref = entry.ref;
+
+ if (entry.action != null) {
+ $entry.tabIndex = tabIndex++;
+
+ const $selectContainer = document.createElement('div');
+ $selectContainer.classList.add('entry-action', 'select-container');
+ $entry.appendChild($selectContainer);
+
+ const $select = document.createElement('select');
+ $select.dataset.ref = entry.ref;
+ $select.name = 'action';
+ $select.tabIndex = tabIndex++;
+
+ for (const action of rebaseActions) {
+ const option = document.createElement('option');
+ option.value = action;
+ option.text = action;
+
+ if (entry.action === action) {
+ option.selected = true;
+ }
+
+ $select.appendChild(option);
+ }
+ $selectContainer.appendChild($select);
+ }
+
+ const $message = document.createElement('span');
+ $message.classList.add('entry-message');
+ $message.innerText = entry.message ?? '';
+ $entry.appendChild($message);
+
+ const commit = state.commits.find(c => c.ref.startsWith(entry.ref));
+ if (commit) {
+ $message.title = commit.message ?? '';
+
+ if (commit.author) {
+ const author = state.authors.find(a => a.author === commit.author);
+ if (author?.avatarUrl.length) {
+ const $avatar = document.createElement('img');
+ $avatar.classList.add('entry-avatar');
+ $avatar.src = author.avatarUrl;
+ $entry.appendChild($avatar);
+ }
+
+ const $author = document.createElement('span');
+ $author.classList.add('entry-author');
+ $author.innerText = commit.author;
+ $entry.appendChild($author);
+ }
+
+ if (commit.dateFromNow) {
+ const $date = document.createElement('span');
+ $date.title = commit.date ?? '';
+ $date.classList.add('entry-date');
+ $date.innerText = commit.dateFromNow;
+ $entry.appendChild($date);
+ }
+ }
+
+ const $ref = document.createElement('a');
+ $ref.classList.add('entry-ref');
+ // $ref.dataset.prev = prev ? `${prev} \u2190 ` : '';
+ $ref.href = commit?.ref ? state.commands.commit.replace(this.commitTokenRegex, commit.ref) : '#';
+ $ref.innerText = entry.ref;
+ $ref.tabIndex = tabIndex++;
+ $entry.appendChild($ref);
+
+ return [$entry, tabIndex];
+ }
+}
+
+new RebaseEditor();
diff --git a/src/webviews/apps/scss/rebase.scss b/src/webviews/apps/scss/rebase.scss
new file mode 100644
index 0000000..bd0ff85
--- /dev/null
+++ b/src/webviews/apps/scss/rebase.scss
@@ -0,0 +1,195 @@
+@import 'base';
+@import 'buttons';
+@import 'utils';
+
+body {
+ height: unset;
+}
+
+.container {
+ display: grid;
+ font-size: 1.3em;
+ grid-template-areas: 'header' 'entries' 'shortcuts' 'actions';
+ grid-template-columns: repeat(1, 1fr min-content);
+ margin: 1em auto;
+ grid-gap: 0.5em 3em;
+ max-width: 1200px;
+ min-width: 450px;
+
+ @media all and (max-width: 768px) {
+ grid-gap: 0.5em 0;
+ }
+}
+
+header {
+ display: flex;
+ align-items: baseline;
+ margin: 0 0 1em 0;
+}
+
+h4 {
+ font-size: 1em;
+ opacity: 0.8;
+}
+
+.entries {
+ grid-area: entries;
+}
+
+.shortcuts {
+ grid-area: shortcuts;
+ margin: 0 auto;
+}
+
+.actions {
+ grid-area: actions;
+ margin: 10px;
+}
+
+.entry {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin: 0 10px;
+ padding: 10px 0;
+
+ &:focus-within {
+ outline: -webkit-focus-ring-color auto 1px;
+ }
+
+ &.entry--base {
+ .vscode-dark & {
+ background: rgba(255, 255, 255, 0.1);
+ }
+
+ .vscode-light & {
+ background: rgba(0, 0, 0, 0.1);
+ }
+
+ &:focus,
+ &:focus-within {
+ outline: none !important;
+ }
+ }
+}
+
+.entry-action {
+ flex: auto 0 0;
+ margin: 0 10px;
+
+ .entry--edit > &,
+ .entry--reword > & {
+ & > select {
+ border: 1px solid rgba(0, 153, 0, 1) !important;
+ outline-color: rgba(0, 153, 0, 1) !important;
+ }
+ }
+
+ .entry--squash > &,
+ .entry--fixup > & {
+ & > select {
+ border: 1px solid rgba(212, 153, 0, 1) !important;
+ outline-color: rgba(212, 153, 0, 1) !important;
+ }
+ }
+
+ .entry--drop > & {
+ & > select {
+ border: 1px solid rgba(153, 0, 0, 1) !important;
+ outline-color: rgba(153, 0, 0, 1) !important;
+ }
+ }
+}
+
+.entry-message {
+ flex: 100% 1 1;
+ margin: 0 10px;
+ position: relative;
+ overflow: hidden;
+ 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;
+ }
+}
+
+.entry-avatar {
+ flex: auto 0 0;
+ margin: 0 -5px 0 0;
+
+ .entry--drop & {
+ // text-decoration: line-through;
+ opacity: 0.25;
+ }
+}
+
+.entry-author,
+.entry-date,
+.entry-ref {
+ flex: auto 0 0;
+ margin: 0 10px;
+ opacity: 0.5;
+
+ .entry--drop & {
+ text-decoration: line-through;
+ opacity: 0.25;
+ }
+}
+
+.shortcut {
+ display: inline-block;
+ margin: 5px 10px 5px 0;
+ opacity: 0.6;
+
+ & span {
+ margin: 0 0 0 5px;
+ }
+}
+
+.branch {
+ &::before {
+ content: '\ea68';
+ font-family: codicon;
+ position: relative;
+ top: 2px;
+ margin: 0 3px;
+ }
+}
+
+.commit {
+ &::before {
+ content: '\eafc';
+ font-family: codicon;
+ position: relative;
+ top: 2px;
+ margin: 0 1px 0 -1px;
+ }
+}
diff --git a/src/webviews/protocol.ts b/src/webviews/protocol.ts
index d6174bf..cd5ab3a 100644
--- a/src/webviews/protocol.ts
+++ b/src/webviews/protocol.ts
@@ -71,3 +71,66 @@ export interface SettingsState extends AppStateWithConfig {
}
export type WelcomeState = AppStateWithConfig;
+
+export interface Author {
+ readonly author: string;
+ readonly avatarUrl: string;
+ readonly email: string | undefined;
+}
+
+export interface Commit {
+ readonly ref: string;
+ readonly author: string;
+ // readonly avatarUrl: string;
+ readonly date: string;
+ readonly dateFromNow: string;
+ // readonly email: string | undefined;
+ readonly message: string;
+ // readonly command: string;
+}
+
+export type RebaseEntryAction = 'pick' | 'reword' | 'edit' | 'squash' | 'fixup' | 'break' | 'drop';
+
+export interface RebaseEntry {
+ readonly action: RebaseEntryAction;
+ readonly ref: string;
+ readonly message: string;
+ readonly index: number;
+}
+
+export interface RebaseDidChangeNotificationParams {
+ entries: RebaseEntry[];
+}
+export const RebaseDidChangeNotificationType = new IpcNotificationType(
+ 'rebase/change',
+);
+
+export const RebaseDidStartCommandType = new IpcCommandType('rebase/start');
+
+export const RebaseDidAbortCommandType = new IpcCommandType('rebase/abort');
+
+export interface RebaseDidChangeEntryCommandParams {
+ ref: string;
+ action: RebaseEntryAction;
+}
+export const RebaseDidChangeEntryCommandType = new IpcCommandType(
+ 'rebase/change/entry',
+);
+
+export interface RebaseDidMoveEntryCommandParams {
+ ref: string;
+ down: boolean;
+}
+export const RebaseDidMoveEntryCommandType = new IpcCommandType('rebase/move/entry');
+
+export interface RebaseState extends RebaseDidChangeNotificationParams {
+ branch: string;
+ onto: string;
+
+ entries: RebaseEntry[];
+ authors: Author[];
+ commits: Commit[];
+ commands: {
+ commit: string;
+ };
+}
diff --git a/src/webviews/rebaseEditor.ts b/src/webviews/rebaseEditor.ts
new file mode 100644
index 0000000..ec54660
--- /dev/null
+++ b/src/webviews/rebaseEditor.ts
@@ -0,0 +1,346 @@
+'use strict';
+import * as paths from 'path';
+import * as fs from 'fs';
+import {
+ CancellationToken,
+ commands,
+ CustomTextEditorProvider,
+ Disposable,
+ Position,
+ Range,
+ TextDocument,
+ Uri,
+ Webview,
+ WebviewPanel,
+ window,
+ workspace,
+ WorkspaceEdit,
+} from 'vscode';
+import { ShowQuickCommitCommand } from '../commands';
+import { Container } from '../container';
+import { Logger } from '../logger';
+import {
+ Author,
+ Commit,
+ IpcMessage,
+ onIpcCommand,
+ RebaseDidAbortCommandType,
+ RebaseDidChangeEntryCommandType,
+ RebaseDidChangeNotificationType,
+ RebaseDidMoveEntryCommandType,
+ RebaseDidStartCommandType,
+ RebaseEntry,
+ RebaseEntryAction,
+ RebaseState,
+} from './protocol';
+
+let ipcSequence = 0;
+function nextIpcId() {
+ if (ipcSequence === Number.MAX_SAFE_INTEGER) {
+ ipcSequence = 1;
+ } else {
+ ipcSequence++;
+ }
+
+ return `host:${ipcSequence}`;
+}
+
+const rebaseRegex = /^\s?#\s?Rebase\s([0-9a-f]+?)..([0-9a-f]+?)\sonto\s([0-9a-f]+?)\s.*$/im;
+const rebaseCommandsRegex = /^\s?(p|pick|r|reword|e|edit|s|squash|f|fixup|d|drop)\s([0-9a-f]+?)\s(.*)$/gm;
+
+const rebaseActionsMap = new Map([
+ ['p', 'pick'],
+ ['pick', 'pick'],
+ ['r', 'reword'],
+ ['reword', 'reword'],
+ ['e', 'edit'],
+ ['edit', 'edit'],
+ ['s', 'squash'],
+ ['squash', 'squash'],
+ ['f', 'fixup'],
+ ['fixup', 'fixup'],
+ ['d', 'drop'],
+ ['drop', 'drop'],
+]);
+
+export class RebaseEditorProvider implements CustomTextEditorProvider, Disposable {
+ private readonly _disposable: Disposable;
+
+ constructor() {
+ this._disposable = Disposable.from(
+ window.registerCustomEditorProvider('gitlens.rebase', this, {
+ webviewOptions: {
+ enableFindWidget: true,
+ },
+ }),
+ );
+ }
+
+ dispose() {
+ this._disposable.dispose();
+ }
+
+ async resolveCustomTextEditor(document: TextDocument, panel: WebviewPanel, _token: CancellationToken) {
+ const disposables: Disposable[] = [];
+
+ disposables.push(panel.onDidDispose(() => disposables.forEach(d => d.dispose())));
+
+ panel.webview.options = { enableCommandUris: true, enableScripts: true };
+
+ disposables.push(panel.webview.onDidReceiveMessage(e => this.onMessageReceived(document, panel, e)));
+
+ disposables.push(
+ workspace.onDidChangeTextDocument(e => {
+ if (e.contentChanges.length === 0 || e.document.uri.toString() !== document.uri.toString()) return;
+
+ this.parseEntriesAndSendChange(panel, document);
+ }),
+ );
+
+ panel.webview.html = await this.getHtml(panel.webview, document);
+ }
+
+ private parseEntries(contents: string): RebaseEntry[];
+ private parseEntries(document: TextDocument): RebaseEntry[];
+ private parseEntries(contentsOrDocument: string | TextDocument): RebaseEntry[] {
+ const contents = typeof contentsOrDocument === 'string' ? contentsOrDocument : contentsOrDocument.getText();
+
+ const entries: RebaseEntry[] = [];
+
+ let match;
+ let action;
+ let ref;
+ let message;
+
+ do {
+ match = rebaseCommandsRegex.exec(contents);
+ if (match == null) break;
+
+ [, action, ref, message] = match;
+
+ entries.push({
+ index: match.index,
+ action: rebaseActionsMap.get(action) ?? 'pick',
+ // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869
+ ref: ` ${ref}`.substr(1),
+ // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869
+ message: message == null || message.length === 0 ? '' : ` ${message}`.substr(1),
+ });
+ } while (true);
+
+ return entries;
+ }
+
+ private parseEntriesAndSendChange(panel: WebviewPanel, document: TextDocument) {
+ const entries = this.parseEntries(document);
+ void this.postMessage(panel, {
+ id: nextIpcId(),
+ method: RebaseDidChangeNotificationType.method,
+ params: { entries: entries },
+ });
+ }
+
+ private async parseState(document: TextDocument): Promise {
+ const repoPath = await Container.git.getRepoPath(paths.join(document.uri.fsPath, '../../..'));
+ const branch = await Container.git.getBranch(repoPath);
+
+ const contents = document.getText();
+ const entries = this.parseEntries(contents);
+ const [, onto] = rebaseRegex.exec(contents) ?? ['', '', ''];
+
+ const authors = new Map();
+ const commits: Commit[] = [];
+
+ let commit = await Container.git.getCommit(repoPath!, onto);
+ if (commit != null) {
+ if (!authors.has(commit.author)) {
+ authors.set(commit.author, {
+ author: commit.author,
+ avatarUrl: commit.getAvatarUri(Container.config.defaultGravatarsStyle).toString(true),
+ email: commit.email,
+ });
+ }
+
+ commits.push({
+ ref: commit.ref,
+ author: commit.author,
+ date: commit.formatDate(Container.config.defaultDateFormat),
+ dateFromNow: commit.formatDateFromNow(),
+ message: commit.message,
+ // command: `command:${Commands.ShowQuickCommitDetails}`,
+ // command: ShowQuickCommitDetailsCommand.getMarkdownCommandArgs({
+ // sha: commit.ref,
+ // }),
+ });
+ }
+
+ for (const entry of entries) {
+ commit = await Container.git.getCommit(repoPath!, entry.ref);
+ if (commit == null) continue;
+
+ if (!authors.has(commit.author)) {
+ authors.set(commit.author, {
+ author: commit.author,
+ avatarUrl: commit.getAvatarUri(Container.config.defaultGravatarsStyle).toString(true),
+ email: commit.email,
+ });
+ }
+
+ commits.push({
+ ref: commit.ref,
+ author: commit.author,
+ date: commit.formatDate(Container.config.defaultDateFormat),
+ dateFromNow: commit.formatDateFromNow(),
+ message: commit.message,
+ // command: `command:${Commands.ShowQuickCommitDetails}`,
+ // command: ShowQuickCommitDetailsCommand.getMarkdownCommandArgs({
+ // sha: commit.ref,
+ // }),
+ });
+ }
+
+ return {
+ branch: branch?.name ?? '',
+ onto: onto ?? '',
+ entries: entries,
+ authors: [...authors.values()],
+ commits: commits,
+ commands: {
+ // eslint-disable-next-line no-template-curly-in-string
+ commit: ShowQuickCommitCommand.getMarkdownCommandArgs('${commit}', repoPath),
+ },
+ };
+ }
+
+ private async postMessage(panel: WebviewPanel, message: IpcMessage) {
+ try {
+ const success = await panel.webview.postMessage(message);
+ return success;
+ } catch (ex) {
+ Logger.error(ex);
+ return false;
+ }
+ }
+
+ private onMessageReceived(document: TextDocument, panel: WebviewPanel, e: IpcMessage) {
+ switch (e.method) {
+ // case ReadyCommandType.method:
+ // onIpcCommand(ReadyCommandType, e, params => {
+ // this.parseDocumentAndSendChange(panel, document);
+ // });
+
+ // break;
+
+ case RebaseDidStartCommandType.method:
+ onIpcCommand(RebaseDidStartCommandType, e, async _params => {
+ await document.save();
+ await commands.executeCommand('workbench.action.closeActiveEditor');
+ });
+
+ break;
+
+ case RebaseDidAbortCommandType.method:
+ onIpcCommand(RebaseDidAbortCommandType, e, async _params => {
+ // Delete the contents to abort the rebase
+ const edit = new WorkspaceEdit();
+ edit.replace(document.uri, new Range(0, 0, document.lineCount, 0), '');
+ await workspace.applyEdit(edit);
+ await document.save();
+ await commands.executeCommand('workbench.action.closeActiveEditor');
+ });
+
+ break;
+
+ case RebaseDidChangeEntryCommandType.method:
+ onIpcCommand(RebaseDidChangeEntryCommandType, e, async params => {
+ const entries = this.parseEntries(document);
+
+ const entry = entries.find(e => e.ref === params.ref);
+ if (entry == null) return;
+
+ const start = document.positionAt(entry.index);
+ const range = document.validateRange(
+ new Range(new Position(start.line, 0), new Position(start.line, Number.MAX_SAFE_INTEGER)),
+ );
+
+ const edit = new WorkspaceEdit();
+ edit.replace(document.uri, range, `${params.action} ${entry.ref} ${entry.message}`);
+ await workspace.applyEdit(edit);
+ });
+
+ break;
+
+ case RebaseDidMoveEntryCommandType.method:
+ onIpcCommand(RebaseDidMoveEntryCommandType, e, async params => {
+ const entries = this.parseEntries(document);
+
+ const entry = entries.find(e => e.ref === params.ref);
+ 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;
+ }
+
+ const start = document.positionAt(entry.index);
+ const range = document.validateRange(
+ new Range(new Position(start.line, 0), new Position(start.line + 1, 0)),
+ );
+
+ const edit = new WorkspaceEdit();
+ 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`,
+ );
+ await workspace.applyEdit(edit);
+ });
+
+ break;
+ }
+ }
+
+ private _html: string | undefined;
+ private async getHtml(webview: Webview, document: TextDocument): Promise {
+ const filename = Container.context.asAbsolutePath(paths.join('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((resolve, reject) => {
+ fs.readFile(filename, 'utf8', (err, data) => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(data);
+ }
+ });
+ });
+ } else {
+ if (this._html !== undefined) return this._html;
+
+ const doc = await workspace.openTextDocument(filename);
+ content = doc.getText();
+ }
+
+ let html = content
+ .replace(/#{cspSource}/g, webview.cspSource)
+ .replace(
+ /#{root}/g,
+ Uri.file(Container.context.asAbsolutePath('.')).with({ scheme: 'vscode-resource' }).toString(),
+ );
+
+ const bootstrap = await this.parseState(document);
+
+ html = html.replace(
+ /#{endOfBody}/i,
+ ``,
+ );
+
+ this._html = html;
+ return html;
+ }
+}
diff --git a/webpack.config.js b/webpack.config.js
index e85ba61..da0d44d 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -247,6 +247,35 @@ function getWebviewsConfig(mode, env) {
filename: '[name].css',
}),
new HtmlPlugin({
+ template: 'rebase/rebase.html',
+ chunks: ['rebase', 'rebase-styles'],
+ excludeAssets: [/.+-styles\.js/],
+ filename: path.resolve(__dirname, 'dist/webviews/rebase.html'),
+ inject: true,
+ inlineSource: mode === 'production' ? '.css$' : undefined,
+ cspPlugin: {
+ enabled: true,
+ policy: cspPolicy,
+ nonceEnabled: {
+ 'script-src': true,
+ 'style-src': true,
+ },
+ },
+ minify:
+ mode === 'production'
+ ? {
+ removeComments: true,
+ collapseWhitespace: true,
+ removeRedundantAttributes: false,
+ useShortDoctype: true,
+ removeEmptyAttributes: true,
+ removeStyleLinkTypeAttributes: true,
+ keepClosingSlash: true,
+ minifyCSS: true,
+ }
+ : false,
+ }),
+ new HtmlPlugin({
template: 'settings/settings.html',
chunks: ['settings', 'settings-styles'],
excludeAssets: [/.+-styles\.js/],
@@ -328,6 +357,8 @@ function getWebviewsConfig(mode, env) {
name: 'webviews',
context: path.resolve(__dirname, 'src/webviews/apps'),
entry: {
+ rebase: ['./rebase/rebase.ts'],
+ 'rebase-styles': ['./scss/rebase.scss'],
settings: ['./settings/settings.ts'],
'settings-styles': ['./scss/settings.scss'],
welcome: ['./welcome/welcome.ts'],