Browse Source

Updates commit details to new tree component

main
Keith Daulton 1 year ago
parent
commit
2c85af90e1
7 changed files with 522 additions and 231 deletions
  1. +32
    -37
      src/webviews/apps/commitDetails/commitDetails.ts
  2. +16
    -0
      src/webviews/apps/commitDetails/components/commit-details-app.ts
  3. +39
    -0
      src/webviews/apps/commitDetails/components/gl-commit-details.ts
  4. +417
    -192
      src/webviews/apps/commitDetails/components/gl-details-base.ts
  5. +14
    -0
      src/webviews/apps/commitDetails/components/gl-wip-details.ts
  6. +3
    -1
      src/webviews/commitDetails/commitDetailsWebview.ts
  7. +1
    -1
      src/webviews/commitDetails/protocol.ts

+ 32
- 37
src/webviews/apps/commitDetails/commitDetails.ts View File

@ -27,10 +27,11 @@ import {
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 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';
@ -61,27 +62,6 @@ export class CommitDetailsApp extends App> {
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')),
@ -92,7 +72,7 @@ export class CommitDetailsApp extends App> {
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('[data-action="create-patch"]', 'click', _e => this.onCreatePatchFromWip(true)),
DOM.on<WebviewPane, WebviewPaneExpandedChangeEventDetail>(
'[data-region="rich-pane"]',
'expanded-change',
@ -100,6 +80,33 @@ export class CommitDetailsApp extends App> {
),
DOM.on('[data-action="explain-commit"]', 'click', e => this.onExplainCommit(e)),
DOM.on('[data-action="switch-ai"]', 'click', e => this.onSwitchAiModel(e)),
DOM.on<GlCommitDetails, { checked: boolean | 'staged' }>('gl-wip-details', 'create-patch', e =>
this.onCreatePatchFromWip(e.detail.checked),
),
DOM.on<GlCommitDetails, FileChangeListItemDetail>('gl-commit-details', 'file-open-on-remote', e =>
this.onOpenFileOnRemote(e.detail),
),
DOM.on<GlCommitDetails, FileChangeListItemDetail>('gl-commit-details,gl-wip-details', 'file-open', e =>
this.onOpenFile(e.detail),
),
DOM.on<GlCommitDetails, FileChangeListItemDetail>('gl-commit-details', 'file-compare-working', e =>
this.onCompareFileWithWorking(e.detail),
),
DOM.on<GlCommitDetails, FileChangeListItemDetail>(
'gl-commit-details,gl-wip-details',
'file-compare-previous',
e => this.onCompareFileWithPrevious(e.detail),
),
DOM.on<GlCommitDetails, FileChangeListItemDetail>('gl-commit-details', 'file-more-actions', e =>
this.onFileMoreActions(e.detail),
),
DOM.on<GlCommitDetails, FileChangeListItemDetail>('gl-wip-details', 'file-stage', e =>
this.onStageFile(e.detail),
),
DOM.on<GlCommitDetails, FileChangeListItemDetail>('gl-wip-details', 'file-unstage', e =>
this.onUnstageFile(e.detail),
),
];
return disposables;
@ -153,21 +160,9 @@ export class CommitDetailsApp extends App> {
}
}
private onCreatePatchFromWip(e: MouseEvent) {
private onCreatePatchFromWip(checked: boolean | 'staged' = true) {
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 });
this.sendCommand(CreatePatchFromWipCommandType, { changes: this.state.wip?.changes, checked: checked });
}
private onCommandClickedCore(action?: string) {

+ 16
- 0
src/webviews/apps/commitDetails/components/commit-details-app.ts View File

@ -68,6 +68,22 @@ export class GlCommitDetailsApp extends LitElement {
defineGkElement(Badge);
}
private indentPreference = 16;
private updateDocumentProperties() {
const preference = this.state?.preferences?.indent;
if (preference === this.indentPreference) return;
this.indentPreference = preference ?? 16;
const rootStyle = document.documentElement.style;
rootStyle.setProperty('--gitlens-tree-indent', `${this.indentPreference}px`);
}
override updated(changedProperties: Map<string | number | symbol, unknown>) {
if (changedProperties.has('state')) {
this.updateDocumentProperties();
}
}
override render() {
const wip = this.state?.wip;

+ 39
- 0
src/webviews/apps/commitDetails/components/gl-commit-details.ts View File

@ -8,7 +8,9 @@ import type { PullRequestShape } from '../../../../git/models/pullRequest';
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 type { File } from './gl-details-base';
import { GlDetailsBase } from './gl-details-base';
interface ExplainState {
@ -522,4 +524,41 @@ export class GlCommitDetails extends GlDetailsBase {
const { added, deleted, changed } = stats.changedFiles;
return html`<commit-stats added="${added}" modified="${changed}" removed="${deleted}"></commit-stats>`;
}
override getFileActions(_file: File, _options?: Partial<TreeItemBase>): TreeItemAction[] {
const actions = [
{
icon: 'go-to-file',
label: 'Open file',
action: 'file-open',
},
];
if (this.isUncommitted) {
return actions;
}
actions.push({
icon: 'git-compare',
label: 'Open Changes with Working File',
action: 'file-compare-working',
});
if (!this.isStash) {
actions.push(
{
icon: 'globe',
label: 'Open on remote',
action: 'file-open-on-remote',
},
{
icon: 'ellipsis',
label: 'Show more actions',
action: 'file-more-actions',
},
);
}
return actions;
}
}

+ 417
- 192
src/webviews/apps/commitDetails/components/gl-details-base.ts View File

@ -1,16 +1,31 @@
import type { TemplateResult } from 'lit';
import { html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { when } from 'lit/directives/when.js';
import type { TextDocumentShowOptions } from 'vscode';
import type { HierarchicalItem } from '../../../../system/array';
import { makeHierarchical } from '../../../../system/array';
import type { Preferences, State } from '../../../commitDetails/protocol';
import type {
TreeItemAction,
TreeItemActionDetail,
TreeItemBase,
TreeItemCheckedDetail,
TreeItemSelectionDetail,
TreeModel,
} from '../../shared/components/tree/base';
import '../../shared/components/tree/tree-generator';
type Files = Mutable<NonNullable<NonNullable<State['commit']>['files']>>;
type File = Files[0];
export type File = Files[0];
type Mode = 'commit' | 'stash' | 'wip';
// Can only import types from 'vscode'
const BesideViewColumn = -2; /*ViewColumn.Beside*/
export interface FileChangeListItemDetail extends File {
showOptions?: TextDocumentShowOptions;
}
export class GlDetailsBase extends LitElement {
readonly tab: 'wip' | 'commit' = 'commit';
@ -26,165 +41,43 @@ export class GlDetailsBase extends LitElement {
@property({ attribute: 'empty-text' })
emptyText? = 'No Files';
private renderWipCategory(staged = true, hasFiles = true) {
const label = staged ? 'Staged Changes' : 'Unstaged Changes';
const shareLabel = `Share ${label}`;
return html`
<list-item tree branch hide-icon>
${label}
${when(
this.tab === 'wip',
() =>
html` <span slot="actions"
><a
class="change-list__action ${!hasFiles ? 'is-disabled' : ''}"
href="#"
title="${shareLabel}"
aria-label="${shareLabel}"
data-action="create-patch"
data-wip-checked="${staged ? 'staged' : 'true'}"
><code-icon icon="gl-cloud-patch-share"></code-icon></a
></span>`,
)}</list-item
>
`;
get fileLayout() {
return this.preferences?.files?.layout ?? 'auto';
}
private renderFileList(mode: Mode, files: Files) {
let items;
let classes;
if (this.isUncommitted) {
items = [];
classes = `indentGuides-${this.preferences?.indentGuides}`;
const staged = files.filter(f => f.staged);
if (staged.length) {
// items.push(html`<list-item tree branch hide-icon>Staged Changes</list-item>`);
items.push(this.renderWipCategory(true, true));
for (const f of staged) {
items.push(this.renderFile(mode, f, 2, true));
}
}
const unstaged = files.filter(f => !f.staged);
if (unstaged.length) {
// items.push(html`<list-item tree branch hide-icon>Unstaged Changes</list-item>`);
items.push(this.renderWipCategory(false, true));
for (const f of unstaged) {
items.push(this.renderFile(mode, f, 2, true));
}
}
} else {
items = files.map(f => this.renderFile(mode, f));
}
return html`<list-container class=${ifDefined(classes)}>${items}</list-container>`;
get isCompact() {
return this.preferences?.files?.compact ?? true;
}
private renderFileTree(mode: Mode, files: Files) {
const compact = this.preferences?.files?.compact ?? true;
let items;
if (this.isUncommitted) {
items = [];
const staged = files.filter(f => f.staged);
if (staged.length) {
// items.push(html`<list-item tree branch hide-icon>Staged Changes</list-item>`);
items.push(this.renderWipCategory(true, true));
items.push(...this.renderFileSubtree(mode, staged, 1, compact));
}
const unstaged = files.filter(f => !f.staged);
if (unstaged.length) {
// items.push(html`<list-item tree branch hide-icon>Unstaged Changes</list-item>`);
items.push(this.renderWipCategory(false, true));
items.push(...this.renderFileSubtree(mode, unstaged, 1, compact));
}
} else {
items = this.renderFileSubtree(mode, files, 0, compact);
}
return html`<list-container class="indentGuides-${this.preferences?.indentGuides}">${items}</list-container>`;
}
private renderFileSubtree(mode: Mode, files: Files, rootLevel: number, compact: boolean) {
const tree = makeHierarchical(
files,
n => n.path.split('/'),
(...parts: string[]) => parts.join('/'),
compact,
);
const flatTree = flattenHeirarchy(tree);
return flatTree.map(({ level, item }) => {
if (item.name === '') return undefined;
if (item.value == null) {
return html`
<list-item level="${rootLevel + level}" tree branch>
<code-icon slot="icon" icon="folder" title="Directory" aria-label="Directory"></code-icon>
${item.name}
</list-item>
`;
}
return this.renderFile(mode, item.value, rootLevel + level, true);
});
}
private renderFile(mode: Mode, file: File, level: number = 1, tree: boolean = false): TemplateResult<1> {
return html`
<file-change-list-item
?tree=${tree}
level="${level}"
?stash=${mode === 'stash'}
?uncommitted=${this.isUncommitted}
?readonly=${this.isUncommitted && mode !== 'wip'}
path="${file.path}"
repo="${file.repoPath}"
?staged=${file.staged}
status="${file.status}"
></file-change-list-item>
`;
get indentGuides(): 'none' | 'onHover' | 'always' {
return this.preferences?.indentGuides ?? 'none';
}
protected renderChangedFiles(mode: Mode, subtitle?: TemplateResult<1>) {
const layout = this.preferences?.files?.layout ?? 'auto';
const isTree = this.isTree(this.files?.length ?? 0);
let value = 'tree';
let icon = 'list-tree';
let label = 'View as Tree';
let isTree = false;
if (this.preferences != null && this.files != null) {
if (layout === 'auto') {
isTree = this.files.length > (this.preferences.files?.threshold ?? 5);
} else {
isTree = layout === 'tree';
}
switch (layout) {
case 'auto':
value = 'list';
icon = 'gl-list-auto';
label = 'View as List';
break;
case 'list':
value = 'tree';
icon = 'list-flat';
label = 'View as Tree';
break;
case 'tree':
value = 'auto';
icon = 'list-tree';
label = 'View as Auto';
break;
}
switch (this.fileLayout) {
case 'auto':
value = 'list';
icon = 'gl-list-auto';
label = 'View as List';
break;
case 'list':
value = 'tree';
icon = 'list-flat';
label = 'View as Tree';
break;
case 'tree':
value = 'auto';
icon = 'list-tree';
label = 'View as Auto';
break;
}
const treeModel = this.createTreeModel(mode, this.files ?? [], isTree, this.isCompact);
return html`
<webview-pane collapsable expanded>
<span slot="title">Files changed </span>
@ -198,31 +91,7 @@ export class GlDetailsBase extends LitElement {
></action-item>
</action-nav>
<div class="change-list" data-region="files">
${when(
this.files == null,
() => html`
<div class="section section--skeleton">
<skeleton-loader></skeleton-loader>
</div>
<div class="section section--skeleton">
<skeleton-loader></skeleton-loader>
</div>
<div class="section section--skeleton">
<skeleton-loader></skeleton-loader>
</div>
`,
() =>
when(
this.files!.length > 0,
() =>
isTree
? this.renderFileTree(mode, this.files!)
: this.renderFileList(mode, this.files!),
() => html`<div class="section"><p>${this.emptyText}</p></div>`,
),
)}
</div>
${this.renderTreeFileModel(treeModel)}
</webview-pane>
`;
}
@ -240,36 +109,392 @@ export class GlDetailsBase extends LitElement {
protected override createRenderRoot() {
return this;
}
}
function flattenHeirarchy<T>(item: HierarchicalItem<T>, level = 0): { level: number; item: HierarchicalItem<T> }[] {
const flattened: { level: number; item: HierarchicalItem<T> }[] = [];
if (item == null) return flattened;
// Tree Model changes
protected isTree(count: number) {
if (this.fileLayout === 'auto') {
return count > (this.preferences?.files?.threshold ?? 5);
}
return this.fileLayout === 'tree';
}
flattened.push({ level: level, item: item });
protected createTreeModel(mode: Mode, files: Files, isTree = false, compact = true): TreeModel[] {
if (!this.isUncommitted) {
return this.createFileTreeModel(mode, files, isTree, compact);
}
if (item.children != null) {
const children = Array.from(item.children.values());
children.sort((a, b) => {
if (!a.value || !b.value) {
return (a.value ? 1 : -1) - (b.value ? 1 : -1);
const children = [];
const staged: Files = [];
const unstaged: Files = [];
for (const f of files) {
if (f.staged) {
staged.push(f);
} else {
unstaged.push(f);
}
}
if (a.relativePath < b.relativePath) {
return -1;
if (staged.length === 0 || unstaged.length === 0) {
children.push(...this.createFileTreeModel(mode, files, isTree, compact));
} else {
if (staged.length) {
children.push({
label: 'Staged Changes',
path: '',
level: 1, // isMulti ? 2 : 1,
branch: true,
checkable: false,
expanded: true,
checked: false, // change.checked !== false,
// disableCheck: true,
context: ['staged'],
children: this.createFileTreeModel(mode, staged, isTree, compact, { level: 2 }),
actions: this.getStagedActions(),
});
}
if (a.relativePath > b.relativePath) {
return 1;
if (unstaged.length) {
children.push({
label: 'Unstaged Changes',
path: '',
level: 1, // isMulti ? 2 : 1,
branch: true,
checkable: false,
expanded: true,
checked: false, // change.checked === true,
context: ['unstaged'],
children: this.createFileTreeModel(mode, unstaged, isTree, compact, { level: 2 }),
actions: this.getUnstagedActions(),
});
}
}
return children;
}
protected sortChildren(children: TreeModel[]): TreeModel[] {
children.sort((a, b) => {
if (a.branch && !b.branch) return -1;
if (!a.branch && b.branch) return 1;
if (a.label < b.label) return -1;
if (a.label > b.label) return 1;
return 0;
});
children.forEach(child => {
flattened.push(...flattenHeirarchy(child, level + 1));
return children;
}
protected createFileTreeModel(
mode: Mode,
files: Files,
isTree = false,
compact = true,
options: Partial<TreeItemBase> = { level: 1 },
): TreeModel[] {
if (options.level === undefined) {
options.level = 1;
}
if (!files.length) {
return [
{
label: 'No changes',
path: '',
level: options.level,
branch: false,
checkable: false,
expanded: true,
checked: false,
},
];
}
const children: TreeModel[] = [];
if (isTree) {
const fileTree = makeHierarchical(
files,
n => n.path.split('/'),
(...parts: string[]) => parts.join('/'),
compact,
);
if (fileTree.children != null) {
for (const child of fileTree.children.values()) {
const childModel = this.walkFileTree(child, { level: options.level });
children.push(childModel);
}
}
} else {
for (const file of files) {
const child = this.fileToTreeModel(file, { level: options.level, branch: false }, true);
children.push(child);
}
}
this.sortChildren(children);
return children;
}
protected walkFileTree(item: HierarchicalItem<File>, options: Partial<TreeItemBase> = { level: 1 }): TreeModel {
if (options.level === undefined) {
options.level = 1;
}
let model: TreeModel;
if (item.value == null) {
model = this.folderToTreeModel(item.name, options);
} else {
model = this.fileToTreeModel(item.value, options);
}
if (item.children != null) {
const children = [];
for (const child of item.children.values()) {
const childModel = this.walkFileTree(child, { ...options, level: options.level + 1 });
children.push(childModel);
}
if (children.length > 0) {
this.sortChildren(children);
model.branch = true;
model.children = children;
}
}
return model;
}
protected getStagedActions(_options?: Partial<TreeItemBase>): TreeItemAction[] {
if (this.tab === 'wip') {
return [
{
icon: 'gl-cloud-patch-share',
label: 'Share Staged Changes',
action: 'staged-create-patch',
},
];
}
return [];
}
protected getUnstagedActions(_options?: Partial<TreeItemBase>): TreeItemAction[] {
if (this.tab === 'wip') {
return [
{
icon: 'gl-cloud-patch-share',
label: 'Share Unstaged Changes',
action: 'unstaged-create-patch',
},
];
}
return [];
}
protected getFileActions(_file: File, _options?: Partial<TreeItemBase>): TreeItemAction[] {
return [];
}
protected fileToTreeModel(
file: File,
options?: Partial<TreeItemBase>,
flat = false,
glue = '/',
): TreeModel<File[]> {
const pathIndex = file.path.lastIndexOf(glue);
const fileName = pathIndex !== -1 ? file.path.substring(pathIndex + 1) : file.path;
const filePath = flat && pathIndex !== -1 ? file.path.substring(0, pathIndex) : '';
return {
branch: false,
expanded: true,
path: file.path,
level: 1,
checkable: false,
checked: false,
icon: 'file', //{ type: 'status', name: file.status },
label: fileName,
description: flat === true ? filePath : undefined,
context: [file],
actions: this.getFileActions(file, options),
decorations: [{ type: 'text', label: file.status }],
...options,
};
}
protected folderToTreeModel(name: string, options?: Partial<TreeItemBase>): TreeModel {
return {
branch: false,
expanded: true,
path: name,
level: 1,
checkable: false,
checked: false,
icon: 'folder',
label: name,
...options,
};
}
protected renderTreeFileModel(treeModel: TreeModel[]) {
return html`<gl-tree-generator
.model=${treeModel}
.guides=${this.indentGuides}
@gl-tree-generated-item-action-clicked=${this.onTreeItemActionClicked}
@gl-tree-generated-item-checked=${this.onTreeItemChecked}
@gl-tree-generated-item-selected=${this.onTreeItemSelected}
></gl-tree-generator>`;
}
// Tree Model action events
// protected onTreeItemActionClicked?(_e: CustomEvent<TreeItemActionDetail>): void;
protected onTreeItemActionClicked(e: CustomEvent<TreeItemActionDetail>) {
if (!e.detail.context || !e.detail.action) return;
const action = e.detail.action;
switch (action.action) {
// stage actions
case 'staged-create-patch':
this.onCreatePatch(e);
break;
case 'unstaged-create-patch':
this.onCreatePatch(e, true);
break;
// file actions
case 'file-open':
this.onOpenFile(e);
break;
case 'file-unstage':
this.onUnstageFile(e);
break;
case 'file-stage':
this.onStageFile(e);
break;
case 'file-compare-working':
this.onCompareWorking(e);
break;
case 'file-open-on-remote':
this.onOpenFileOnRemote(e);
break;
case 'file-more-actions':
this.onMoreActions(e);
break;
}
}
protected onTreeItemChecked?(_e: CustomEvent<TreeItemCheckedDetail>): void;
// protected onTreeItemSelected?(_e: CustomEvent<TreeItemSelectionDetail>): void;
protected onTreeItemSelected(e: CustomEvent<TreeItemSelectionDetail>) {
if (!e.detail.context) return;
this.onComparePrevious(e);
}
onCreatePatch(_e: CustomEvent<TreeItemActionDetail>, isAll = false) {
const event = new CustomEvent('create-patch', {
detail: {
checked: isAll ? true : 'staged',
},
});
this.dispatchEvent(event);
}
onOpenFile(e: CustomEvent<TreeItemActionDetail>) {
if (!e.detail.context) return;
const [file] = e.detail.context;
const event = new CustomEvent('file-open', {
detail: this.getEventDetail(file, {
preview: false,
viewColumn: e.detail.altKey ? BesideViewColumn : undefined,
}),
});
this.dispatchEvent(event);
}
onOpenFileOnRemote(e: CustomEvent<TreeItemActionDetail>) {
if (!e.detail.context) return;
const [file] = e.detail.context;
const event = new CustomEvent('file-open-on-remote', {
detail: this.getEventDetail(file, {
preview: false,
viewColumn: e.detail.altKey ? BesideViewColumn : undefined,
}),
});
this.dispatchEvent(event);
}
onCompareWorking(e: CustomEvent<TreeItemActionDetail>) {
if (!e.detail.context) return;
const [file] = e.detail.context;
const event = new CustomEvent('file-compare-working', {
detail: this.getEventDetail(file, {
preview: false,
viewColumn: e.detail.altKey ? BesideViewColumn : undefined,
}),
});
this.dispatchEvent(event);
}
onComparePrevious(e: CustomEvent<TreeItemSelectionDetail>) {
if (!e.detail.context) return;
const [file] = e.detail.context;
const event = new CustomEvent('file-compare-previous', {
detail: this.getEventDetail(file, {
preview: false,
viewColumn: e.detail.altKey ? BesideViewColumn : undefined,
}),
});
this.dispatchEvent(event);
}
onMoreActions(e: CustomEvent<TreeItemActionDetail>) {
if (!e.detail.context) return;
const [file] = e.detail.context;
const event = new CustomEvent('file-more-actions', {
detail: this.getEventDetail(file),
});
this.dispatchEvent(event);
}
onStageFile(e: CustomEvent<TreeItemActionDetail>) {
if (!e.detail.context) return;
const [file] = e.detail.context;
const event = new CustomEvent('file-stage', {
detail: this.getEventDetail(file, {
preview: false,
viewColumn: e.detail.altKey ? BesideViewColumn : undefined,
}),
});
this.dispatchEvent(event);
}
return flattened;
onUnstageFile(e: CustomEvent<TreeItemActionDetail>) {
if (!e.detail.context) return;
const [file] = e.detail.context;
const event = new CustomEvent('file-unstage', {
detail: this.getEventDetail(file, {
preview: false,
viewColumn: e.detail.altKey ? BesideViewColumn : undefined,
}),
});
this.dispatchEvent(event);
}
private getEventDetail(file: File, showOptions?: TextDocumentShowOptions): FileChangeListItemDetail {
return {
path: file.path,
repoPath: file.repoPath,
status: file.status,
// originalPath: this.originalPath,
staged: file.staged,
showOptions: showOptions,
};
}
}

+ 14
- 0
src/webviews/apps/commitDetails/components/gl-wip-details.ts View File

@ -3,6 +3,8 @@ import { customElement, property } from 'lit/decorators.js';
import { when } from 'lit/directives/when.js';
import { pluralize } from '../../../../system/string';
import type { Wip } from '../../../commitDetails/protocol';
import type { TreeItemAction, TreeItemBase } from '../../shared/components/tree/base';
import type { File } from './gl-details-base';
import { GlDetailsBase } from './gl-details-base';
@customElement('gl-wip-details')
@ -76,4 +78,16 @@ export class GlWipDetails extends GlDetailsBase {
${this.renderChangedFiles('wip')}
`;
}
override getFileActions(file: File, _options?: Partial<TreeItemBase>): TreeItemAction[] {
const openFile = {
icon: 'go-to-file',
label: 'Open file',
action: 'file-open',
};
if (file.staged === true) {
return [openFile, { icon: 'remove', label: 'Unstage changes', action: 'file-unstage' }];
}
return [openFile, { icon: 'plus', label: 'Stage changes', action: 'file-stage' }];
}
}

+ 3
- 1
src/webviews/commitDetails/commitDetailsWebview.ts View File

@ -399,7 +399,8 @@ export class CommitDetailsWebviewProvider
'views.commitDetails.files',
'views.commitDetails.avatars',
]) ||
configuration.changedAny<CoreConfiguration>(e, 'workbench.tree.renderIndentGuides')
configuration.changedAny<CoreConfiguration>(e, 'workbench.tree.renderIndentGuides') ||
configuration.changedAny<CoreConfiguration>(e, 'workbench.tree.indent')
) {
this.updatePendingContext({
preferences: {
@ -430,6 +431,7 @@ export class CommitDetailsWebviewProvider
configuration.getAny<CoreConfiguration, Preferences['indentGuides']>(
'workbench.tree.renderIndentGuides',
) ?? 'onHover',
indent: configuration.getAny<CoreConfiguration, Preferences['indent']>('workbench.tree.indent'),
};
}

+ 1
- 1
src/webviews/commitDetails/protocol.ts View File

@ -37,7 +37,7 @@ export interface Preferences {
avatars: boolean;
dateFormat: DateTimeFormat | string;
files: Config['views']['commitDetails']['files'];
// indent: number;
indent: number | undefined;
indentGuides: 'none' | 'onHover' | 'always';
}
export type UpdateablePreferences = Partial<Pick<Preferences, 'autolinksExpanded' | 'files'>>;

Loading…
Cancel
Save