Browse Source

Adds custom editor for rebasing

main
Eric Amodio 4 years ago
parent
commit
66a3ab7c94
8 changed files with 992 additions and 0 deletions
  1. +11
    -0
      package.json
  2. +2
    -0
      src/container.ts
  3. +30
    -0
      src/webviews/apps/rebase/rebase.html
  4. +314
    -0
      src/webviews/apps/rebase/rebase.ts
  5. +195
    -0
      src/webviews/apps/scss/rebase.scss
  6. +63
    -0
      src/webviews/protocol.ts
  7. +346
    -0
      src/webviews/rebaseEditor.ts
  8. +31
    -0
      webpack.config.js

+ 11
- 0
package.json View File

@ -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",

+ 2
- 0
src/container.ts View File

@ -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));

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

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
</head>
<body class="preload">
<div class="container">
<header>
<h2>Interactive Rebase</h2>
<h4 id="subhead"></h4>
</header>
<ul id="entries" class="entries"></ul>
<div class="shortcuts">
<span class="shortcut"><kbd>p</kbd><span>Pick</span></span>
<span class="shortcut"><kbd>r</kbd><span>Reword</span></span>
<span class="shortcut"><kbd>e</kbd><span>Edit</span></span>
<span class="shortcut"><kbd>s</kbd><span>Squash</span></span>
<span class="shortcut"><kbd>d</kbd><span>Drop</span></span>
<span class="shortcut"><kbd>alt ↑</kbd><span>Move Up</span></span>
<span class="shortcut"><kbd>alt ↓</kbd><span>Move Down</span></span>
</div>
<div class="actions">
<button name="start" class="button button--flat-primary" data-action="start">Start Rebase</button>
<button name="abort" class="button button--flat" data-action="abort">Abort</button>
</div>
</div>
#{endOfBody}
</body>
</html>

+ 314
- 0
src/webviews/apps/rebase/rebase.ts View File

@ -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<string, RebaseEntryAction>([
['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<RebaseState> {
// 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<HTMLSelectElement>('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<HTMLLIElement>(`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<HTMLSelectElement>(
'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 = `<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>`;
const $container = document.getElementById('entries')!;
const focusRef = document.activeElement?.closest<HTMLLIElement>('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<HTMLLIElement>(
`${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();

+ 195
- 0
src/webviews/apps/scss/rebase.scss View File

@ -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;
}
}

+ 63
- 0
src/webviews/protocol.ts View File

@ -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<RebaseDidChangeNotificationParams>(
'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<RebaseDidChangeEntryCommandParams>(
'rebase/change/entry',
);
export interface RebaseDidMoveEntryCommandParams {
ref: string;
down: boolean;
}
export const RebaseDidMoveEntryCommandType = new IpcCommandType<RebaseDidMoveEntryCommandParams>('rebase/move/entry');
export interface RebaseState extends RebaseDidChangeNotificationParams {
branch: string;
onto: string;
entries: RebaseEntry[];
authors: Author[];
commits: Commit[];
commands: {
commit: string;
};
}

+ 346
- 0
src/webviews/rebaseEditor.ts View File

@ -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<string, RebaseEntryAction>([
['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<RebaseState> {
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<string, Author>();
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<string> {
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<string>((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,
`<script type="text/javascript" nonce="Z2l0bGVucy1ib290c3RyYXA=">window.bootstrap = ${JSON.stringify(
bootstrap,
)};</script>`,
);
this._html = html;
return html;
}
}

+ 31
- 0
webpack.config.js View File

@ -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'],

Loading…
Cancel
Save