Browse Source

Shares html token replacement code

Joins html & bootstrap/etc loading together for better perf
Fixes incorrect placement token replacement
main
Eric Amodio 1 year ago
parent
commit
3bf719633a
4 changed files with 197 additions and 195 deletions
  1. +1
    -1
      src/webviews/apps/plus/timeline/timeline.scss
  2. +113
    -149
      src/webviews/rebase/rebaseEditor.ts
  3. +82
    -45
      src/webviews/webviewController.ts
  4. +1
    -0
      src/webviews/webviewsController.ts

+ 1
- 1
src/webviews/apps/plus/timeline/timeline.scss View File

@ -21,7 +21,7 @@ body {
overflow-x: scroll;
}
body[data-placement='editor'] {
body[data-placement='tab'] {
background-color: var(--color-background);
}

+ 113
- 149
src/webviews/rebase/rebaseEditor.ts View File

@ -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[];

+ 82
- 45
src/webviews/webviewController.ts View File

@ -18,9 +18,16 @@ import { serialize } from '../system/decorators/serialize';
import type { TrackedUsageFeatures } from '../telemetry/usageTracker';
import type { IpcMessage, IpcMessageParams, IpcNotificationType, WebviewFocusChangedParams } from './protocol';
import { ExecuteCommandType, onIpc, WebviewFocusChangedCommandType, WebviewReadyCommandType } from './protocol';
import type { WebviewIds, WebviewPanelDescriptor, WebviewViewDescriptor, WebviewViewIds } from './webviewsController';
import type {
CustomEditorIds,
WebviewIds,
WebviewPanelDescriptor,
WebviewViewDescriptor,
WebviewViewIds,
} from './webviewsController';
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() {
@ -390,52 +397,27 @@ export class WebviewController implements Dispos
private async getHtml(webview: Webview): Promise<string> {
const webRootUri = this.getWebRootUri();
const uri = Uri.joinPath(webRootUri, this.fileName);
const content = new TextDecoder('utf8').decode(await workspace.fs.readFile(uri));
const [bootstrap, head, body, endOfBody] = await Promise.all([
const [bytes, bootstrap, head, body, endOfBody] = await Promise.all([
workspace.fs.readFile(uri),
this.provider.includeBootstrap?.(),
this.provider.includeHead?.(),
this.provider.includeBody?.(),
this.provider.includeEndOfBody?.(),
]);
const cspSource = webview.cspSource;
const root = this.asWebviewUri(this.getRootUri()).toString();
const webRoot = this.getWebRoot();
const html = content.replace(
/#{(head|body|endOfBody|placement|cspSource|cspNonce|root|webroot)}/g,
(_substring: string, token: string) => {
switch (token) {
case 'head':
return head ?? '';
case 'body':
return body ?? '';
case 'endOfBody':
return `${
bootstrap != null
? `<script type="text/javascript" nonce="${
this.cspNonce
}">window.bootstrap=${JSON.stringify(bootstrap)};</script>`
: ''
}${endOfBody ?? ''}`;
case 'placement':
return 'view';
case 'cspSource':
return cspSource;
case 'cspNonce':
return this.cspNonce;
case 'root':
return root;
case 'webroot':
return webRoot;
default:
return '';
}
},
const html = replaceWebviewHtmlTokens(
utf8TextDecoder.decode(bytes),
webview.cspSource,
this._cspNonce,
this.asWebviewUri(this.getRootUri()).toString(),
this.getWebRoot(),
this.type,
bootstrap,
head,
body,
endOfBody,
);
return html;
}
@ -473,25 +455,80 @@ export class WebviewController implements Dispos
}
}
function resetContextKeys(
contextKeyPrefix: `${ContextKeys.WebviewPrefix}${WebviewIds}` | `${ContextKeys.WebviewViewPrefix}${WebviewViewIds}`,
export function replaceWebviewHtmlTokens<SerializedState>(
html: string,
cspSource: string,
cspNonce: string,
root: string,
webRoot: string,
placement: WebviewController<any>['type'],
bootstrap?: SerializedState,
head?: string,
body?: string,
endOfBody?: string,
) {
return html.replace(
/#{(head|body|endOfBody|placement|cspSource|cspNonce|root|webroot)}/g,
(_substring: string, token: string) => {
switch (token) {
case 'head':
return head ?? '';
case 'body':
return body ?? '';
case 'endOfBody':
return `${
bootstrap != null
? `<script type="text/javascript" nonce="${cspNonce}">window.bootstrap=${JSON.stringify(
bootstrap,
)};</script>`
: ''
}${endOfBody ?? ''}`;
case 'placement':
return placement;
case 'cspSource':
return cspSource;
case 'cspNonce':
return cspNonce;
case 'root':
return root;
case 'webroot':
return webRoot;
default:
return '';
}
},
);
}
export function resetContextKeys(
contextKeyPrefix:
| `${ContextKeys.WebviewPrefix}${WebviewIds | CustomEditorIds}`
| `${ContextKeys.WebviewViewPrefix}${WebviewViewIds}`,
): void {
void setContext(`${contextKeyPrefix}:inputFocus`, false);
void setContext(`${contextKeyPrefix}:focus`, false);
if (contextKeyPrefix.startsWith(ContextKeys.WebviewPrefix)) {
void setContext(`${contextKeyPrefix as `${ContextKeys.WebviewPrefix}${WebviewIds}`}:active`, false);
void setContext(
`${contextKeyPrefix as `${ContextKeys.WebviewPrefix}${WebviewIds | CustomEditorIds}`}:active`,
false,
);
}
}
function setContextKeys(
contextKeyPrefix: `${ContextKeys.WebviewPrefix}${WebviewIds}` | `${ContextKeys.WebviewViewPrefix}${WebviewViewIds}`,
export function setContextKeys(
contextKeyPrefix:
| `${ContextKeys.WebviewPrefix}${WebviewIds | CustomEditorIds}`
| `${ContextKeys.WebviewViewPrefix}${WebviewViewIds}`,
active?: boolean,
focus?: boolean,
inputFocus?: boolean,
): void {
if (contextKeyPrefix.startsWith(ContextKeys.WebviewPrefix)) {
if (active != null) {
void setContext(`${contextKeyPrefix as `${ContextKeys.WebviewPrefix}${WebviewIds}`}:active`, active);
void setContext(
`${contextKeyPrefix as `${ContextKeys.WebviewPrefix}${WebviewIds | CustomEditorIds}`}:active`,
active,
);
if (!active) {
focus = false;

+ 1
- 0
src/webviews/webviewsController.ts View File

@ -13,6 +13,7 @@ import type { TrackedUsageFeatures } from '../telemetry/usageTracker';
import type { WebviewProvider } from './webviewController';
import { WebviewController } from './webviewController';
export type CustomEditorIds = 'rebaseEditor';
export type WebviewIds = 'graph' | 'settings' | 'timeline' | 'welcome' | 'focus';
export type WebviewViewIds = 'commitDetails' | 'graph' | 'home' | 'timeline';

Loading…
Cancel
Save