|
/*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 { App } from '../shared/appBase';
|
|
import type { FileChangeListItem, FileChangeListItemDetail } from '../shared/components/list/file-change-list-item';
|
|
import type { WebviewPane, WebviewPaneExpandedChangeEventDetail } from '../shared/components/webview-pane';
|
|
import { DOM } from '../shared/dom';
|
|
import type { GlCommitDetailsApp } from './components/commit-details-app';
|
|
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/formatted-date';
|
|
import '../shared/components/rich/issue-pull-request';
|
|
import '../shared/components/skeleton-loader';
|
|
import '../shared/components/commit/commit-stats';
|
|
import '../shared/components/webview-pane';
|
|
import '../shared/components/progress';
|
|
import '../shared/components/list/list-container';
|
|
import '../shared/components/list/list-item';
|
|
import '../shared/components/list/file-change-list-item';
|
|
import './components/commit-details-app';
|
|
|
|
export const uncommittedSha = '0000000000000000000000000000000000000000';
|
|
|
|
export type CommitState = SomeNonNullable<Serialized<State>, 'commit'>;
|
|
export class CommitDetailsApp extends App<Serialized<State>> {
|
|
constructor() {
|
|
super('CommitDetailsApp');
|
|
}
|
|
|
|
override onInitialize() {
|
|
this.attachState();
|
|
}
|
|
|
|
override onBind() {
|
|
const disposables = [
|
|
DOM.on<FileChangeListItem, FileChangeListItemDetail>('file-change-list-item', 'file-open-on-remote', e =>
|
|
this.onOpenFileOnRemote(e.detail),
|
|
),
|
|
DOM.on<FileChangeListItem, FileChangeListItemDetail>('file-change-list-item', 'file-open', e =>
|
|
this.onOpenFile(e.detail),
|
|
),
|
|
DOM.on<FileChangeListItem, FileChangeListItemDetail>('file-change-list-item', 'file-compare-working', e =>
|
|
this.onCompareFileWithWorking(e.detail),
|
|
),
|
|
DOM.on<FileChangeListItem, FileChangeListItemDetail>('file-change-list-item', 'file-compare-previous', e =>
|
|
this.onCompareFileWithPrevious(e.detail),
|
|
),
|
|
DOM.on<FileChangeListItem, FileChangeListItemDetail>('file-change-list-item', 'file-more-actions', e =>
|
|
this.onFileMoreActions(e.detail),
|
|
),
|
|
DOM.on<FileChangeListItem, FileChangeListItemDetail>('file-change-list-item', 'file-stage', e =>
|
|
this.onStageFile(e.detail),
|
|
),
|
|
DOM.on<FileChangeListItem, FileChangeListItemDetail>('file-change-list-item', 'file-unstage', e =>
|
|
this.onUnstageFile(e.detail),
|
|
),
|
|
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(e)),
|
|
DOM.on<WebviewPane, WebviewPaneExpandedChangeEventDetail>(
|
|
'[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)),
|
|
];
|
|
|
|
return disposables;
|
|
}
|
|
|
|
protected override onMessageReceived(e: MessageEvent) {
|
|
const msg = e.data as IpcMessage;
|
|
this.log(`onMessageReceived(${msg.id}): name=${msg.method}`);
|
|
|
|
switch (msg.method) {
|
|
// case DidChangeRichStateNotificationType.method:
|
|
// onIpc(DidChangeRichStateNotificationType, msg, params => {
|
|
// if (this.state.selected == null) return;
|
|
|
|
// assertsSerialized<typeof params>(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<State>(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?.(e);
|
|
}
|
|
}
|
|
|
|
private onCreatePatchFromWip(e: MouseEvent) {
|
|
if (this.state.wip?.changes == null) return;
|
|
|
|
const wipCheckedParam = ((e.target as HTMLElement)?.closest('[data-wip-checked]') as HTMLElement | undefined)
|
|
?.dataset.wipChecked;
|
|
let wipChecked: boolean | 'staged';
|
|
if (wipCheckedParam == null) {
|
|
wipChecked = true;
|
|
} else if (wipCheckedParam === 'staged') {
|
|
wipChecked = wipCheckedParam;
|
|
} else {
|
|
wipChecked = wipCheckedParam === 'true';
|
|
}
|
|
|
|
this.sendCommand(CreatePatchFromWipCommandType, { changes: this.state.wip?.changes, checked: wipChecked });
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
function assertsSerialized<T>(obj: unknown): asserts obj is Serialized<T> {}
|
|
|
|
new CommitDetailsApp();
|