|
|
@ -10,7 +10,6 @@ import { getNonce } from '@env/crypto'; |
|
|
|
import { ShowCommitsInViewCommand } from '../../commands'; |
|
|
|
import { ContextKeys, CoreCommands } from '../../constants'; |
|
|
|
import type { Container } from '../../container'; |
|
|
|
import { setContext } from '../../context'; |
|
|
|
import { emojify } from '../../emojis'; |
|
|
|
import type { GitCommit } from '../../git/models/commit'; |
|
|
|
import { createReference } from '../../git/models/reference'; |
|
|
@ -26,6 +25,7 @@ import { Logger } from '../../system/logger'; |
|
|
|
import { normalizePath } from '../../system/path'; |
|
|
|
import type { IpcMessage, WebviewFocusChangedParams } from '../protocol'; |
|
|
|
import { onIpc, WebviewFocusChangedCommandType } from '../protocol'; |
|
|
|
import { replaceWebviewHtmlTokens, resetContextKeys, setContextKeys } from '../webviewController'; |
|
|
|
import type { |
|
|
|
Author, |
|
|
|
ChangeEntryParams, |
|
|
@ -50,6 +50,7 @@ import { |
|
|
|
} from './protocol'; |
|
|
|
|
|
|
|
const maxSmallIntegerV8 = 2 ** 30; // Max number that can be stored in V8's smis (small integers)
|
|
|
|
const utf8TextDecoder = new TextDecoder('utf8'); |
|
|
|
|
|
|
|
let ipcSequence = 0; |
|
|
|
function nextIpcId() { |
|
|
@ -208,7 +209,7 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl |
|
|
|
|
|
|
|
subscriptions.push( |
|
|
|
panel.onDidDispose(() => { |
|
|
|
this.resetContextKeys(); |
|
|
|
resetContextKeys(this.contextKeyPrefix); |
|
|
|
|
|
|
|
Disposable.from(...subscriptions).dispose(); |
|
|
|
}), |
|
|
@ -245,34 +246,11 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private resetContextKeys(): void { |
|
|
|
void setContext(`${this.contextKeyPrefix}:inputFocus`, false); |
|
|
|
void setContext(`${this.contextKeyPrefix}:focus`, false); |
|
|
|
void setContext(`${this.contextKeyPrefix}:active`, false); |
|
|
|
} |
|
|
|
|
|
|
|
private setContextKeys(active: boolean | undefined, focus?: boolean, inputFocus?: boolean): void { |
|
|
|
if (active != null) { |
|
|
|
void setContext(`${this.contextKeyPrefix}:active`, active); |
|
|
|
|
|
|
|
if (!active) { |
|
|
|
focus = false; |
|
|
|
inputFocus = false; |
|
|
|
} |
|
|
|
} |
|
|
|
if (focus != null) { |
|
|
|
void setContext(`${this.contextKeyPrefix}:focus`, focus); |
|
|
|
} |
|
|
|
if (inputFocus != null) { |
|
|
|
void setContext(`${this.contextKeyPrefix}:inputFocus`, inputFocus); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@debug<RebaseEditorProvider['onViewFocusChanged']>({ |
|
|
|
args: { 0: e => `focused=${e.focused}, inputFocused=${e.inputFocused}` }, |
|
|
|
}) |
|
|
|
protected onViewFocusChanged(e: WebviewFocusChangedParams): void { |
|
|
|
this.setContextKeys(e.focused, e.focused, e.inputFocused); |
|
|
|
setContextKeys(this.contextKeyPrefix, e.focused, e.focused, e.inputFocused); |
|
|
|
} |
|
|
|
|
|
|
|
@debug<RebaseEditorProvider['onViewStateChanged']>({ |
|
|
@ -284,9 +262,9 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl |
|
|
|
protected onViewStateChanged(context: RebaseEditorContext, e: WebviewPanelOnDidChangeViewStateEvent): void { |
|
|
|
const { active, visible } = e.webviewPanel; |
|
|
|
if (visible) { |
|
|
|
this.setContextKeys(active); |
|
|
|
setContextKeys(this.contextKeyPrefix, active); |
|
|
|
} else { |
|
|
|
this.resetContextKeys(); |
|
|
|
resetContextKeys(this.contextKeyPrefix); |
|
|
|
} |
|
|
|
|
|
|
|
if (!context.pendingChange) return; |
|
|
@ -299,7 +277,7 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl |
|
|
|
const branch = await this.container.git.getBranch(context.repoPath); |
|
|
|
context.branchName = branch?.name ?? null; |
|
|
|
} |
|
|
|
const state = await this.parseRebaseTodo(context); |
|
|
|
const state = await parseRebaseTodo(this.container, context, this.ascending); |
|
|
|
return state; |
|
|
|
} |
|
|
|
|
|
|
@ -515,7 +493,7 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl |
|
|
|
'commit:selected', |
|
|
|
{ |
|
|
|
commit: createReference(sha, context.repoPath, { refType: 'revision' }), |
|
|
|
pin: false, |
|
|
|
interaction: 'passive', |
|
|
|
preserveFocus: true, |
|
|
|
preserveVisibility: context.firstSelection |
|
|
|
? showDetailsView === false |
|
|
@ -613,140 +591,126 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl |
|
|
|
private async getHtml(context: RebaseEditorContext): Promise<string> { |
|
|
|
const webRootUri = Uri.joinPath(this.container.context.extensionUri, 'dist', 'webviews'); |
|
|
|
const uri = Uri.joinPath(webRootUri, 'rebase.html'); |
|
|
|
const content = new TextDecoder('utf8').decode(await workspace.fs.readFile(uri)); |
|
|
|
|
|
|
|
const bootstrap = await this.parseState(context); |
|
|
|
const cspSource = context.panel.webview.cspSource; |
|
|
|
const cspNonce = getNonce(); |
|
|
|
|
|
|
|
const root = context.panel.webview.asWebviewUri(this.container.context.extensionUri).toString(); |
|
|
|
const webRoot = context.panel.webview.asWebviewUri(webRootUri).toString(); |
|
|
|
|
|
|
|
const html = content.replace( |
|
|
|
/#{(head|body|endOfBody|placement|cspSource|cspNonce|root|webroot)}/g, |
|
|
|
(_substring: string, token: string) => { |
|
|
|
switch (token) { |
|
|
|
case 'endOfBody': |
|
|
|
return `<script type="text/javascript" nonce="${cspNonce}">window.bootstrap=${JSON.stringify( |
|
|
|
bootstrap, |
|
|
|
)};</script>`;
|
|
|
|
case 'placement': |
|
|
|
return 'editor'; |
|
|
|
case 'cspSource': |
|
|
|
return cspSource; |
|
|
|
case 'cspNonce': |
|
|
|
return cspNonce; |
|
|
|
case 'root': |
|
|
|
return root; |
|
|
|
case 'webroot': |
|
|
|
return webRoot; |
|
|
|
default: |
|
|
|
return ''; |
|
|
|
} |
|
|
|
}, |
|
|
|
); |
|
|
|
|
|
|
|
const [bytes, bootstrap] = await Promise.all([workspace.fs.readFile(uri), this.parseState(context)]); |
|
|
|
|
|
|
|
const html = replaceWebviewHtmlTokens( |
|
|
|
utf8TextDecoder.decode(bytes), |
|
|
|
context.panel.webview.cspSource, |
|
|
|
getNonce(), |
|
|
|
context.panel.webview.asWebviewUri(this.container.context.extensionUri).toString(), |
|
|
|
context.panel.webview.asWebviewUri(webRootUri).toString(), |
|
|
|
'tab', |
|
|
|
bootstrap, |
|
|
|
); |
|
|
|
return html; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@debug({ args: false }) |
|
|
|
private async parseRebaseTodo(context: RebaseEditorContext): Promise<Omit<State, 'rebasing'>> { |
|
|
|
const contents = context.document.getText(); |
|
|
|
const entries = parseRebaseTodoEntries(contents); |
|
|
|
let [, , , onto] = rebaseRegex.exec(contents) ?? ['', '', '']; |
|
|
|
|
|
|
|
if (context.authors == null || context.commits == null) { |
|
|
|
await this.loadRichCommitData(context, onto, entries); |
|
|
|
async function loadRichCommitData( |
|
|
|
container: Container, |
|
|
|
context: RebaseEditorContext, |
|
|
|
onto: string, |
|
|
|
entries: RebaseEntry[], |
|
|
|
) { |
|
|
|
context.commits = []; |
|
|
|
context.authors = new Map<string, Author>(); |
|
|
|
|
|
|
|
const log = await container.git.richSearchCommits( |
|
|
|
context.repoPath, |
|
|
|
{ |
|
|
|
query: `${onto ? `#:${onto} ` : ''}${join( |
|
|
|
map(entries, e => `#:${e.sha}`), |
|
|
|
' ', |
|
|
|
)}`,
|
|
|
|
}, |
|
|
|
{ limit: 0 }, |
|
|
|
); |
|
|
|
|
|
|
|
if (log != null) { |
|
|
|
for (const c of log.commits.values()) { |
|
|
|
context.commits.push(c); |
|
|
|
|
|
|
|
if (!context.authors.has(c.author.name)) { |
|
|
|
context.authors.set(c.author.name, { |
|
|
|
author: c.author.name, |
|
|
|
avatarUrl: (await c.getAvatarUri()).toString(true), |
|
|
|
email: c.author.email, |
|
|
|
}); |
|
|
|
} |
|
|
|
if (!context.authors.has(c.committer.name)) { |
|
|
|
const avatarUri = await c.committer.getAvatarUri(c); |
|
|
|
context.authors.set(c.committer.name, { |
|
|
|
author: c.committer.name, |
|
|
|
avatarUrl: avatarUri.toString(true), |
|
|
|
email: c.committer.email, |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const defaultDateFormat = configuration.get('defaultDateFormat'); |
|
|
|
const command = ShowCommitsInViewCommand.getMarkdownCommandArgs(`\${commit}`, context.repoPath); |
|
|
|
async function parseRebaseTodo( |
|
|
|
container: Container, |
|
|
|
context: RebaseEditorContext, |
|
|
|
ascending: boolean, |
|
|
|
): Promise<Omit<State, 'rebasing'>> { |
|
|
|
const contents = context.document.getText(); |
|
|
|
const entries = parseRebaseTodoEntries(contents); |
|
|
|
let [, , , onto] = rebaseRegex.exec(contents) ?? ['', '', '']; |
|
|
|
|
|
|
|
if (context.authors == null || context.commits == null) { |
|
|
|
await loadRichCommitData(container, context, onto, entries); |
|
|
|
} |
|
|
|
|
|
|
|
const ontoCommit = onto ? context.commits?.find(c => c.sha.startsWith(onto)) : undefined; |
|
|
|
const defaultDateFormat = configuration.get('defaultDateFormat'); |
|
|
|
const command = ShowCommitsInViewCommand.getMarkdownCommandArgs(`\${commit}`, context.repoPath); |
|
|
|
|
|
|
|
let commit; |
|
|
|
for (const entry of entries) { |
|
|
|
commit = context.commits?.find(c => c.sha.startsWith(entry.sha)); |
|
|
|
if (commit == null) continue; |
|
|
|
const ontoCommit = onto ? context.commits?.find(c => c.sha.startsWith(onto)) : undefined; |
|
|
|
|
|
|
|
// If the onto commit is contained in the list of commits, remove it and clear the 'onto' value — See #1201
|
|
|
|
if (commit.sha === ontoCommit?.sha) { |
|
|
|
onto = ''; |
|
|
|
} |
|
|
|
let commit; |
|
|
|
for (const entry of entries) { |
|
|
|
commit = context.commits?.find(c => c.sha.startsWith(entry.sha)); |
|
|
|
if (commit == null) continue; |
|
|
|
|
|
|
|
entry.commit = { |
|
|
|
sha: commit.sha, |
|
|
|
author: commit.author.name, |
|
|
|
committer: commit.committer.name, |
|
|
|
date: commit.formatDate(defaultDateFormat), |
|
|
|
dateFromNow: commit.formatDateFromNow(), |
|
|
|
message: emojify(commit.message ?? commit.summary), |
|
|
|
}; |
|
|
|
// If the onto commit is contained in the list of commits, remove it and clear the 'onto' value — See #1201
|
|
|
|
if (commit.sha === ontoCommit?.sha) { |
|
|
|
onto = ''; |
|
|
|
} |
|
|
|
|
|
|
|
return { |
|
|
|
branch: context.branchName ?? '', |
|
|
|
onto: onto |
|
|
|
? { |
|
|
|
sha: onto, |
|
|
|
commit: |
|
|
|
ontoCommit != null |
|
|
|
? { |
|
|
|
sha: ontoCommit.sha, |
|
|
|
author: ontoCommit.author.name, |
|
|
|
committer: ontoCommit.committer.name, |
|
|
|
date: ontoCommit.formatDate(defaultDateFormat), |
|
|
|
dateFromNow: ontoCommit.formatDateFromNow(), |
|
|
|
message: emojify(ontoCommit.message || 'root'), |
|
|
|
} |
|
|
|
: undefined, |
|
|
|
} |
|
|
|
: undefined, |
|
|
|
entries: entries, |
|
|
|
authors: context.authors != null ? Object.fromEntries(context.authors) : {}, |
|
|
|
commands: { commit: command }, |
|
|
|
ascending: this.ascending, |
|
|
|
entry.commit = { |
|
|
|
sha: commit.sha, |
|
|
|
author: commit.author.name, |
|
|
|
committer: commit.committer.name, |
|
|
|
date: commit.formatDate(defaultDateFormat), |
|
|
|
dateFromNow: commit.formatDateFromNow(), |
|
|
|
message: emojify(commit.message ?? commit.summary), |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
@debug({ args: false }) |
|
|
|
private async loadRichCommitData(context: RebaseEditorContext, onto: string, entries: RebaseEntry[]) { |
|
|
|
context.commits = []; |
|
|
|
context.authors = new Map<string, Author>(); |
|
|
|
|
|
|
|
const log = await this.container.git.richSearchCommits( |
|
|
|
context.repoPath, |
|
|
|
{ |
|
|
|
query: `${onto ? `#:${onto} ` : ''}${join( |
|
|
|
map(entries, e => `#:${e.sha}`), |
|
|
|
' ', |
|
|
|
)}`,
|
|
|
|
}, |
|
|
|
{ limit: 0 }, |
|
|
|
); |
|
|
|
|
|
|
|
if (log != null) { |
|
|
|
for (const c of log.commits.values()) { |
|
|
|
context.commits.push(c); |
|
|
|
|
|
|
|
if (!context.authors.has(c.author.name)) { |
|
|
|
context.authors.set(c.author.name, { |
|
|
|
author: c.author.name, |
|
|
|
avatarUrl: (await c.getAvatarUri()).toString(true), |
|
|
|
email: c.author.email, |
|
|
|
}); |
|
|
|
} |
|
|
|
if (!context.authors.has(c.committer.name)) { |
|
|
|
const avatarUri = await c.committer.getAvatarUri(c); |
|
|
|
context.authors.set(c.committer.name, { |
|
|
|
author: c.committer.name, |
|
|
|
avatarUrl: avatarUri.toString(true), |
|
|
|
email: c.committer.email, |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
return { |
|
|
|
branch: context.branchName ?? '', |
|
|
|
onto: onto |
|
|
|
? { |
|
|
|
sha: onto, |
|
|
|
commit: |
|
|
|
ontoCommit != null |
|
|
|
? { |
|
|
|
sha: ontoCommit.sha, |
|
|
|
author: ontoCommit.author.name, |
|
|
|
committer: ontoCommit.committer.name, |
|
|
|
date: ontoCommit.formatDate(defaultDateFormat), |
|
|
|
dateFromNow: ontoCommit.formatDateFromNow(), |
|
|
|
message: emojify(ontoCommit.message || 'root'), |
|
|
|
} |
|
|
|
: undefined, |
|
|
|
} |
|
|
|
: undefined, |
|
|
|
entries: entries, |
|
|
|
authors: context.authors != null ? Object.fromEntries(context.authors) : {}, |
|
|
|
commands: { commit: command }, |
|
|
|
ascending: ascending, |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
function parseRebaseTodoEntries(contents: string): RebaseEntry[]; |
|
|
|