From 3bf719633a3e14f47137db84d09ece962adc06c4 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Thu, 23 Mar 2023 00:40:23 -0400 Subject: [PATCH] Shares html token replacement code Joins html & bootstrap/etc loading together for better perf Fixes incorrect placement token replacement --- src/webviews/apps/plus/timeline/timeline.scss | 2 +- src/webviews/rebase/rebaseEditor.ts | 262 +++++++++++--------------- src/webviews/webviewController.ts | 127 ++++++++----- src/webviews/webviewsController.ts | 1 + 4 files changed, 197 insertions(+), 195 deletions(-) diff --git a/src/webviews/apps/plus/timeline/timeline.scss b/src/webviews/apps/plus/timeline/timeline.scss index f624930..1ceacc9 100644 --- a/src/webviews/apps/plus/timeline/timeline.scss +++ b/src/webviews/apps/plus/timeline/timeline.scss @@ -21,7 +21,7 @@ body { overflow-x: scroll; } -body[data-placement='editor'] { +body[data-placement='tab'] { background-color: var(--color-background); } diff --git a/src/webviews/rebase/rebaseEditor.ts b/src/webviews/rebase/rebaseEditor.ts index fde5aa4..f75fe12 100644 --- a/src/webviews/rebase/rebaseEditor.ts +++ b/src/webviews/rebase/rebaseEditor.ts @@ -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({ 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({ @@ -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 { 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 ``; - 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> { - 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(); + + 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> { + 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(); - - 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[]; diff --git a/src/webviews/webviewController.ts b/src/webviews/webviewController.ts index 026903b..f30173f 100644 --- a/src/webviews/webviewController.ts +++ b/src/webviews/webviewController.ts @@ -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 { 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 - ? `` - : '' - }${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( + html: string, + cspSource: string, + cspNonce: string, + root: string, + webRoot: string, + placement: WebviewController['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 + ? `` + : '' + }${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; diff --git a/src/webviews/webviewsController.ts b/src/webviews/webviewsController.ts index c35fe05..dddb7de 100644 --- a/src/webviews/webviewsController.ts +++ b/src/webviews/webviewsController.ts @@ -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';