diff --git a/src/webviews/apps/commitDetails/commitDetails.ts b/src/webviews/apps/commitDetails/commitDetails.ts index 73412dba22ccd..2a021f7883e40 100644 --- a/src/webviews/apps/commitDetails/commitDetails.ts +++ b/src/webviews/apps/commitDetails/commitDetails.ts @@ -1,50 +1,12 @@ /*global*/ -import type { ViewFilesLayout } from '../../../config'; import type { Serialized } from '../../../system/serialize'; -import type { CommitActionsParams, Mode, State } from '../../commitDetails/protocol'; -import { - AutolinkSettingsCommandType, - CommitActionsCommandType, - CreatePatchFromWipCommandType, - DidChangeNotificationType, - DidChangeWipStateNotificationType, - DidExplainCommandType, - ExplainCommandType, - FileActionsCommandType, - NavigateCommitCommandType, - OpenFileCommandType, - OpenFileComparePreviousCommandType, - OpenFileCompareWorkingCommandType, - OpenFileOnRemoteCommandType, - PickCommitCommandType, - PinCommitCommandType, - SearchCommitCommandType, - StageFileCommandType, - SwitchModeCommandType, - UnstageFileCommandType, - UpdatePreferencesCommandType, -} from '../../commitDetails/protocol'; -import type { IpcMessage } from '../../protocol'; -import { ExecuteCommandType, onIpc } from '../../protocol'; +import type { State } from '../../commitDetails/protocol'; import { App } from '../shared/appBase'; -import type { WebviewPane, WebviewPaneExpandedChangeEventDetail } from '../shared/components/webview-pane'; import { DOM } from '../shared/dom'; import type { GlCommitDetailsApp } from './components/commit-details-app'; -import type { GlCommitDetails } from './components/gl-commit-details'; -import type { FileChangeListItemDetail } from './components/gl-details-base'; import './commitDetails.scss'; -import '../shared/components/actions/action-item'; -import '../shared/components/actions/action-nav'; -import '../shared/components/code-icon'; -import '../shared/components/commit/commit-identity'; -import '../shared/components/rich/issue-pull-request'; -import '../shared/components/skeleton-loader'; -import '../shared/components/commit/commit-stats'; -import '../shared/components/webview-pane'; import './components/commit-details-app'; -export const uncommittedSha = '0000000000000000000000000000000000000000'; - export type CommitState = SomeNonNullable, 'commit'>; export class CommitDetailsApp extends App> { constructor() { @@ -52,247 +14,13 @@ export class CommitDetailsApp extends App> { } override onInitialize() { - this.attachState(); - } - - override onBind() { - const disposables = [ - DOM.on('[data-action="commit-actions"]', 'click', e => this.onCommitActions(e)), - DOM.on('[data-action="pick-commit"]', 'click', e => this.onPickCommit(e)), - DOM.on('[data-action="wip"]', 'click', e => this.onSwitchMode(e, 'wip')), - DOM.on('[data-action="details"]', 'click', e => this.onSwitchMode(e, 'commit')), - DOM.on('[data-action="search-commit"]', 'click', e => this.onSearchCommit(e)), - DOM.on('[data-action="autolink-settings"]', 'click', e => this.onAutolinkSettings(e)), - DOM.on('[data-action="files-layout"]', 'click', e => this.onToggleFilesLayout(e)), - DOM.on('[data-action="pin"]', 'click', e => this.onTogglePin(e)), - DOM.on('[data-action="back"]', 'click', e => this.onNavigate('back', e)), - DOM.on('[data-action="forward"]', 'click', e => this.onNavigate('forward', e)), - DOM.on('[data-action="create-patch"]', 'click', _e => this.onCreatePatchFromWip(true)), - DOM.on( - '[data-region="rich-pane"]', - 'expanded-change', - e => this.onExpandedChange(e.detail), - ), - DOM.on('[data-action="explain-commit"]', 'click', e => this.onExplainCommit(e)), - DOM.on('[data-action="switch-ai"]', 'click', e => this.onSwitchAiModel(e)), - DOM.on('gl-wip-details', 'create-patch', e => - this.onCreatePatchFromWip(e.detail.checked), - ), - - DOM.on('gl-commit-details', 'file-open-on-remote', e => - this.onOpenFileOnRemote(e.detail), - ), - DOM.on('gl-commit-details,gl-wip-details', 'file-open', e => - this.onOpenFile(e.detail), - ), - DOM.on('gl-commit-details', 'file-compare-working', e => - this.onCompareFileWithWorking(e.detail), - ), - DOM.on( - 'gl-commit-details,gl-wip-details', - 'file-compare-previous', - e => this.onCompareFileWithPrevious(e.detail), - ), - DOM.on('gl-commit-details', 'file-more-actions', e => - this.onFileMoreActions(e.detail), - ), - DOM.on('gl-wip-details', 'file-stage', e => - this.onStageFile(e.detail), - ), - DOM.on('gl-wip-details', 'file-unstage', e => - this.onUnstageFile(e.detail), - ), - ]; - - return disposables; - } - - protected override onMessageReceived(msg: IpcMessage) { - switch (msg.method) { - // case DidChangeRichStateNotificationType.method: - // onIpc(DidChangeRichStateNotificationType, msg, params => { - // if (this.state.selected == null) return; - - // assertsSerialized(params); - - // const newState = { ...this.state }; - // if (params.formattedMessage != null) { - // newState.selected!.message = params.formattedMessage; - // } - // // if (params.pullRequest != null) { - // newState.pullRequest = params.pullRequest; - // // } - // // if (params.formattedMessage != null) { - // newState.autolinkedIssues = params.autolinkedIssues; - // // } - - // this.state = newState; - // this.setState(this.state); - - // this.renderRichContent(); - // }); - // break; - case DidChangeNotificationType.method: - onIpc(DidChangeNotificationType, msg, params => { - assertsSerialized(params.state); - - this.state = params.state; - this.setState(this.state); - this.attachState(); - }); - break; - - case DidChangeWipStateNotificationType.method: - onIpc(DidChangeWipStateNotificationType, msg, params => { - this.state = { ...this.state, ...params }; - this.setState(this.state); - this.attachState(); - }); - break; - - default: - super.onMessageReceived?.(msg); - } - } - - private onCreatePatchFromWip(checked: boolean | 'staged' = true) { - if (this.state.wip?.changes == null) return; - this.sendCommand(CreatePatchFromWipCommandType, { changes: this.state.wip?.changes, checked: checked }); - } - - private onCommandClickedCore(action?: string) { - const command = action?.startsWith('command:') ? action.slice(8) : action; - if (command == null) return; - - this.sendCommand(ExecuteCommandType, { command: command }); - } - - private onSwitchAiModel(_e: MouseEvent) { - this.onCommandClickedCore('gitlens.switchAIModel'); - } - - async onExplainCommit(_e: MouseEvent) { - try { - const result = await this.sendCommandWithCompletion(ExplainCommandType, undefined, DidExplainCommandType); - - if (result.error) { - this.component.explain = { error: { message: result.error.message ?? 'Error retrieving content' } }; - } else if (result.summary) { - this.component.explain = { summary: result.summary }; - } else { - this.component.explain = undefined; - } - } catch (ex) { - this.component.explain = { error: { message: 'Error retrieving content' } }; - } - } - - private onToggleFilesLayout(e: MouseEvent) { - const layout = ((e.target as HTMLElement)?.dataset.filesLayout as ViewFilesLayout) ?? undefined; - if (layout === this.state.preferences?.files?.layout) return; - - const files = { - ...this.state.preferences?.files, - layout: layout ?? 'auto', - }; - - this.state = { ...this.state, preferences: { ...this.state.preferences, files: files } }; - this.attachState(); - - this.sendCommand(UpdatePreferencesCommandType, { files: files }); - } - - private onExpandedChange(e: WebviewPaneExpandedChangeEventDetail) { - this.state = { ...this.state, preferences: { ...this.state.preferences, autolinksExpanded: e.expanded } }; - this.attachState(); - - this.sendCommand(UpdatePreferencesCommandType, { autolinksExpanded: e.expanded }); - } - - private onNavigate(direction: 'back' | 'forward', e: Event) { - e.preventDefault(); - this.sendCommand(NavigateCommitCommandType, { direction: direction }); - } - - private onTogglePin(e: MouseEvent) { - e.preventDefault(); - this.sendCommand(PinCommitCommandType, { pin: !this.state.pinned }); - } - - private onAutolinkSettings(e: MouseEvent) { - e.preventDefault(); - this.sendCommand(AutolinkSettingsCommandType, undefined); - } - - private onPickCommit(_e: MouseEvent) { - this.sendCommand(PickCommitCommandType, undefined); - } - - private onSearchCommit(_e: MouseEvent) { - this.sendCommand(SearchCommitCommandType, undefined); - } - - private onSwitchMode(_e: MouseEvent, mode: Mode) { - this.state = { ...this.state, mode: mode }; - this.attachState(); - - this.sendCommand(SwitchModeCommandType, { mode: mode, repoPath: this.state.commit?.repoPath }); - } - - private onOpenFileOnRemote(e: FileChangeListItemDetail) { - this.sendCommand(OpenFileOnRemoteCommandType, e); - } - - private onOpenFile(e: FileChangeListItemDetail) { - this.sendCommand(OpenFileCommandType, e); - } - - private onCompareFileWithWorking(e: FileChangeListItemDetail) { - this.sendCommand(OpenFileCompareWorkingCommandType, e); - } - - private onCompareFileWithPrevious(e: FileChangeListItemDetail) { - this.sendCommand(OpenFileComparePreviousCommandType, e); - } - - private onFileMoreActions(e: FileChangeListItemDetail) { - this.sendCommand(FileActionsCommandType, e); - } - - onStageFile(e: FileChangeListItemDetail): void { - this.sendCommand(StageFileCommandType, e); - } - - onUnstageFile(e: FileChangeListItemDetail): void { - this.sendCommand(UnstageFileCommandType, e); - } - - private onCommitActions(e: MouseEvent) { - e.preventDefault(); - if (this.state.commit === undefined) { - e.stopPropagation(); - return; - } - - const action = (e.target as HTMLElement)?.getAttribute('data-action-type'); - if (action == null) return; - - this.sendCommand(CommitActionsCommandType, { action: action as CommitActionsParams['action'], alt: e.altKey }); - } - - private _component?: GlCommitDetailsApp; - private get component() { - if (this._component == null) { - this._component = (document.getElementById('app') as GlCommitDetailsApp)!; - } - return this._component; - } - - attachState() { - this.component.state = this.state; + const component = document.getElementById('app') as GlCommitDetailsApp; + component.state = this.state; + DOM.on>(component, 'state-changed', e => { + this.state = e.detail; + this.setState(this.state); + }); } } -function assertsSerialized(obj: unknown): asserts obj is Serialized {} - new CommitDetailsApp(); diff --git a/src/webviews/apps/commitDetails/components/commit-details-app.ts b/src/webviews/apps/commitDetails/components/commit-details-app.ts index 0916d00c693fe..909c2303a1cde 100644 --- a/src/webviews/apps/commitDetails/components/commit-details-app.ts +++ b/src/webviews/apps/commitDetails/components/commit-details-app.ts @@ -3,13 +3,53 @@ import { html, LitElement } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { when } from 'lit/directives/when.js'; +import type { ViewFilesLayout } from '../../../../config'; import type { Serialized } from '../../../../system/serialize'; import { pluralize } from '../../../../system/string'; -import type { State } from '../../../commitDetails/protocol'; +import type { CommitActionsParams, Mode, State } from '../../../commitDetails/protocol'; +import { + AutolinkSettingsCommandType, + CommitActionsCommandType, + CreatePatchFromWipCommandType, + DidChangeNotificationType, + DidChangeWipStateNotificationType, + DidExplainCommandType, + ExplainCommandType, + FileActionsCommandType, + NavigateCommitCommandType, + OpenFileCommandType, + OpenFileComparePreviousCommandType, + OpenFileCompareWorkingCommandType, + OpenFileOnRemoteCommandType, + PickCommitCommandType, + PinCommitCommandType, + SearchCommitCommandType, + StageFileCommandType, + SwitchModeCommandType, + UnstageFileCommandType, + UpdatePreferencesCommandType, +} from '../../../commitDetails/protocol'; +import type { IpcMessage } from '../../../protocol'; +import { ExecuteCommandType, onIpc } from '../../../protocol'; import '../../shared/components/button'; import './gl-commit-details'; import './gl-wip-details'; -import { uncommittedSha } from '../commitDetails'; +import type { WebviewPane, WebviewPaneExpandedChangeEventDetail } from '../../shared/components/webview-pane'; +import type { Disposable } from '../../shared/dom'; +import { DOM } from '../../shared/dom'; +import { assertsSerialized, HostIpc } from '../../shared/ipc'; +import type { GlCommitDetails } from './gl-commit-details'; +import type { FileChangeListItemDetail } from './gl-details-base'; +import '../../shared/components/actions/action-item'; +import '../../shared/components/actions/action-nav'; +import '../../shared/components/code-icon'; +import '../../shared/components/commit/commit-identity'; +import '../../shared/components/rich/issue-pull-request'; +import '../../shared/components/skeleton-loader'; +import '../../shared/components/commit/commit-stats'; +import '../../shared/components/webview-pane'; + +export const uncommittedSha = '0000000000000000000000000000000000000000'; interface ExplainState { cancelled?: boolean; @@ -62,6 +102,9 @@ export class GlCommitDetailsApp extends LitElement { return actions; } + private _disposables: Disposable[] = []; + private _hostIpc!: HostIpc; + constructor() { super(); @@ -84,6 +127,117 @@ export class GlCommitDetailsApp extends LitElement { } } + override connectedCallback() { + super.connectedCallback(); + + this._hostIpc = new HostIpc('commit-details'); + + this._disposables = [ + this._hostIpc.onReceiveMessage(e => this.onMessageReceived(e)), + this._hostIpc, + + DOM.on('[data-action="commit-actions"]', 'click', e => this.onCommitActions(e)), + DOM.on('[data-action="pick-commit"]', 'click', e => this.onPickCommit(e)), + DOM.on('[data-action="wip"]', 'click', e => this.onSwitchMode(e, 'wip')), + DOM.on('[data-action="details"]', 'click', e => this.onSwitchMode(e, 'commit')), + DOM.on('[data-action="search-commit"]', 'click', e => this.onSearchCommit(e)), + DOM.on('[data-action="autolink-settings"]', 'click', e => this.onAutolinkSettings(e)), + DOM.on('[data-action="files-layout"]', 'click', e => this.onToggleFilesLayout(e)), + DOM.on('[data-action="pin"]', 'click', e => this.onTogglePin(e)), + DOM.on('[data-action="back"]', 'click', e => this.onNavigate('back', e)), + DOM.on('[data-action="forward"]', 'click', e => this.onNavigate('forward', e)), + DOM.on('[data-action="create-patch"]', 'click', _e => this.onCreatePatchFromWip(true)), + DOM.on( + '[data-region="rich-pane"]', + 'expanded-change', + e => this.onExpandedChange(e.detail), + ), + DOM.on('[data-action="explain-commit"]', 'click', e => this.onExplainCommit(e)), + DOM.on('[data-action="switch-ai"]', 'click', e => this.onSwitchAiModel(e)), + DOM.on('gl-wip-details', 'create-patch', e => + this.onCreatePatchFromWip(e.detail.checked), + ), + + DOM.on('gl-commit-details', 'file-open-on-remote', e => + this.onOpenFileOnRemote(e.detail), + ), + DOM.on('gl-commit-details,gl-wip-details', 'file-open', e => + this.onOpenFile(e.detail), + ), + DOM.on('gl-commit-details', 'file-compare-working', e => + this.onCompareFileWithWorking(e.detail), + ), + DOM.on( + 'gl-commit-details,gl-wip-details', + 'file-compare-previous', + e => this.onCompareFileWithPrevious(e.detail), + ), + DOM.on('gl-commit-details', 'file-more-actions', e => + this.onFileMoreActions(e.detail), + ), + DOM.on('gl-wip-details', 'file-stage', e => + this.onStageFile(e.detail), + ), + DOM.on('gl-wip-details', 'file-unstage', e => + this.onUnstageFile(e.detail), + ), + ]; + } + + private onMessageReceived(msg: IpcMessage) { + switch (msg.method) { + // case DidChangeRichStateNotificationType.method: + // onIpc(DidChangeRichStateNotificationType, msg, params => { + // if (this.state.selected == null) return; + + // assertsSerialized(params); + + // const newState = { ...this.state }; + // if (params.formattedMessage != null) { + // newState.selected!.message = params.formattedMessage; + // } + // // if (params.pullRequest != null) { + // newState.pullRequest = params.pullRequest; + // // } + // // if (params.formattedMessage != null) { + // newState.autolinkedIssues = params.autolinkedIssues; + // // } + + // this.state = newState; + // this.setState(this.state); + + // this.renderRichContent(); + // }); + // break; + case DidChangeNotificationType.method: + onIpc(DidChangeNotificationType, msg, params => { + assertsSerialized(params.state); + + this.state = params.state; + this.dispatchEvent(new CustomEvent('state-changed', { detail: this.state })); + // this.setState(this.state); + // this.attachState(); + }); + break; + + case DidChangeWipStateNotificationType.method: + onIpc(DidChangeWipStateNotificationType, msg, params => { + this.state = { ...this.state, ...params } as any; + this.dispatchEvent(new CustomEvent('state-changed', { detail: this.state })); + // this.setState(this.state); + // this.attachState(); + }); + break; + } + } + + override disconnectedCallback() { + this._disposables.forEach(d => d.dispose()); + this._disposables = []; + + super.disconnectedCallback(); + } + override render() { const wip = this.state?.wip; @@ -147,4 +301,138 @@ export class GlCommitDetailsApp extends LitElement { protected override createRenderRoot() { return this; } + + private onCreatePatchFromWip(checked: boolean | 'staged' = true) { + if (this.state?.wip?.changes == null) return; + this._hostIpc.sendCommand(CreatePatchFromWipCommandType, { changes: this.state.wip.changes, checked: checked }); + } + + private onCommandClickedCore(action?: string) { + const command = action?.startsWith('command:') ? action.slice(8) : action; + if (command == null) return; + + this._hostIpc.sendCommand(ExecuteCommandType, { command: command }); + } + + private onSwitchAiModel(_e: MouseEvent) { + this.onCommandClickedCore('gitlens.switchAIModel'); + } + + async onExplainCommit(_e: MouseEvent) { + try { + const result = await this._hostIpc.sendCommandWithCompletion( + ExplainCommandType, + undefined, + DidExplainCommandType, + ); + if (result.error) { + this.explain = { error: { message: result.error.message ?? 'Error retrieving content' } }; + } else if (result.summary) { + this.explain = { summary: result.summary }; + } else { + this.explain = undefined; + } + } catch (ex) { + this.explain = { error: { message: 'Error retrieving content' } }; + } + } + + private onToggleFilesLayout(e: MouseEvent) { + const layout = ((e.target as HTMLElement)?.dataset.filesLayout as ViewFilesLayout) ?? undefined; + if (layout === this.state?.preferences?.files?.layout) return; + + const files = { + ...this.state!.preferences?.files, + layout: layout ?? 'auto', + }; + + this.state = { ...this.state, preferences: { ...this.state!.preferences, files: files } } as any; + // this.attachState(); + + this._hostIpc.sendCommand(UpdatePreferencesCommandType, { files: files }); + } + + private onExpandedChange(e: WebviewPaneExpandedChangeEventDetail) { + this.state = { + ...this.state, + preferences: { ...this.state!.preferences, autolinksExpanded: e.expanded }, + } as any; + // this.attachState(); + + this._hostIpc.sendCommand(UpdatePreferencesCommandType, { autolinksExpanded: e.expanded }); + } + + private onNavigate(direction: 'back' | 'forward', e: Event) { + e.preventDefault(); + this._hostIpc.sendCommand(NavigateCommitCommandType, { direction: direction }); + } + + private onTogglePin(e: MouseEvent) { + e.preventDefault(); + this._hostIpc.sendCommand(PinCommitCommandType, { pin: !this.state!.pinned }); + } + + private onAutolinkSettings(e: MouseEvent) { + e.preventDefault(); + this._hostIpc.sendCommand(AutolinkSettingsCommandType, undefined); + } + + private onPickCommit(_e: MouseEvent) { + this._hostIpc.sendCommand(PickCommitCommandType, undefined); + } + + private onSearchCommit(_e: MouseEvent) { + this._hostIpc.sendCommand(SearchCommitCommandType, undefined); + } + + private onSwitchMode(_e: MouseEvent, mode: Mode) { + this.state = { ...this.state, mode: mode } as any; + // this.attachState(); + + this._hostIpc.sendCommand(SwitchModeCommandType, { mode: mode, repoPath: this.state!.commit?.repoPath }); + } + + private onOpenFileOnRemote(e: FileChangeListItemDetail) { + this._hostIpc.sendCommand(OpenFileOnRemoteCommandType, e); + } + + private onOpenFile(e: FileChangeListItemDetail) { + this._hostIpc.sendCommand(OpenFileCommandType, e); + } + + private onCompareFileWithWorking(e: FileChangeListItemDetail) { + this._hostIpc.sendCommand(OpenFileCompareWorkingCommandType, e); + } + + private onCompareFileWithPrevious(e: FileChangeListItemDetail) { + this._hostIpc.sendCommand(OpenFileComparePreviousCommandType, e); + } + + private onFileMoreActions(e: FileChangeListItemDetail) { + this._hostIpc.sendCommand(FileActionsCommandType, e); + } + + private onStageFile(e: FileChangeListItemDetail): void { + this._hostIpc.sendCommand(StageFileCommandType, e); + } + + private onUnstageFile(e: FileChangeListItemDetail): void { + this._hostIpc.sendCommand(UnstageFileCommandType, e); + } + + private onCommitActions(e: MouseEvent) { + e.preventDefault(); + if (this.state?.commit === undefined) { + e.stopPropagation(); + return; + } + + const action = (e.target as HTMLElement)?.getAttribute('data-action-type'); + if (action == null) return; + + this._hostIpc.sendCommand(CommitActionsCommandType, { + action: action as CommitActionsParams['action'], + alt: e.altKey, + }); + } } diff --git a/src/webviews/apps/commitDetails/components/gl-commit-details.ts b/src/webviews/apps/commitDetails/components/gl-commit-details.ts index 1b6fa5697e647..e418dc1472072 100644 --- a/src/webviews/apps/commitDetails/components/gl-commit-details.ts +++ b/src/webviews/apps/commitDetails/components/gl-commit-details.ts @@ -9,7 +9,7 @@ import type { Serialized } from '../../../../system/serialize'; import type { State } from '../../../commitDetails/protocol'; import { messageHeadlineSplitterToken } from '../../../commitDetails/protocol'; import type { TreeItemAction, TreeItemBase } from '../../shared/components/tree/base'; -import { uncommittedSha } from '../commitDetails'; +import { uncommittedSha } from './commit-details-app'; import type { File } from './gl-details-base'; import { GlDetailsBase } from './gl-details-base'; diff --git a/src/webviews/apps/shared/dom.ts b/src/webviews/apps/shared/dom.ts index cb2d7797e3c57..0c5ad114e01b3 100644 --- a/src/webviews/apps/shared/dom.ts +++ b/src/webviews/apps/shared/dom.ts @@ -24,6 +24,12 @@ export namespace DOM { listener: (e: DocumentEventMap[K] & { target: HTMLElement | null }, target: T) => void, options?: boolean | AddEventListenerOptions, ): Disposable; + export function on( + elemtent: T, + name: string, + listener: (e: CustomEvent & { target: HTMLElement | null }, target: T) => void, + options?: boolean | AddEventListenerOptions, + ): Disposable; export function on( selector: string, name: K,