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