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