mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 04:49:35 +00:00
SCM - Initial implementation of the Sync view (#193440)
This commit is contained in:
parent
e20eb064c6
commit
1545aeab06
|
@ -18,6 +18,7 @@
|
|||
"editSessionIdentityProvider",
|
||||
"quickDiffProvider",
|
||||
"scmActionButton",
|
||||
"scmHistoryProvider",
|
||||
"scmSelectedProvider",
|
||||
"scmValidation",
|
||||
"tabInputTextMerge",
|
||||
|
|
|
@ -20,24 +20,24 @@ interface ActionButtonState {
|
|||
readonly repositoryHasChangesToCommit: boolean;
|
||||
}
|
||||
|
||||
export class ActionButtonCommand {
|
||||
private _onDidChange = new EventEmitter<void>();
|
||||
abstract class AbstractActionButton {
|
||||
protected _onDidChange = new EventEmitter<void>();
|
||||
get onDidChange(): Event<void> { return this._onDidChange.event; }
|
||||
|
||||
private _state: ActionButtonState;
|
||||
private get state() { return this._state; }
|
||||
private set state(state: ActionButtonState) {
|
||||
protected get state() { return this._state; }
|
||||
protected set state(state: ActionButtonState) {
|
||||
if (JSON.stringify(this._state) !== JSON.stringify(state)) {
|
||||
this._state = state;
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
}
|
||||
|
||||
private disposables: Disposable[] = [];
|
||||
abstract get button(): SourceControlActionButton | undefined;
|
||||
|
||||
constructor(
|
||||
readonly repository: Repository,
|
||||
readonly postCommitCommandCenter: CommitCommandsCenter) {
|
||||
protected disposables: Disposable[] = [];
|
||||
|
||||
constructor(readonly repository: Repository) {
|
||||
this._state = {
|
||||
HEAD: undefined,
|
||||
isCheckoutInProgress: false,
|
||||
|
@ -50,6 +50,126 @@ export class ActionButtonCommand {
|
|||
|
||||
repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables);
|
||||
repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables);
|
||||
}
|
||||
|
||||
protected getPublishBranchActionButton(): SourceControlActionButton | undefined {
|
||||
const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(cloud-upload)';
|
||||
|
||||
return {
|
||||
command: {
|
||||
command: 'git.publish',
|
||||
title: l10n.t({ message: '{0} Publish Branch', args: [icon], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }),
|
||||
tooltip: this.state.isSyncInProgress ?
|
||||
(this.state.HEAD?.name ?
|
||||
l10n.t({ message: 'Publishing Branch "{0}"...', args: [this.state.HEAD.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) :
|
||||
l10n.t({ message: 'Publishing Branch...', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })) :
|
||||
(this.repository.HEAD?.name ?
|
||||
l10n.t({ message: 'Publish Branch "{0}"', args: [this.state.HEAD?.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) :
|
||||
l10n.t({ message: 'Publish Branch', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })),
|
||||
arguments: [this.repository.sourceControl],
|
||||
},
|
||||
enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress && !this.state.isCommitInProgress && !this.state.isMergeInProgress && !this.state.isRebaseInProgress
|
||||
};
|
||||
}
|
||||
|
||||
protected getSyncChangesActionButton(): SourceControlActionButton | undefined {
|
||||
const branchIsAheadOrBehind = (this.state.HEAD?.behind ?? 0) > 0 || (this.state.HEAD?.ahead ?? 0) > 0;
|
||||
|
||||
const ahead = this.state.HEAD?.ahead ? ` ${this.state.HEAD.ahead}$(arrow-up)` : '';
|
||||
const behind = this.state.HEAD?.behind ? ` ${this.state.HEAD.behind}$(arrow-down)` : '';
|
||||
const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(sync)';
|
||||
|
||||
return {
|
||||
command: {
|
||||
command: 'git.sync',
|
||||
title: l10n.t('{0} Sync Changes{1}{2}', icon, behind, ahead),
|
||||
tooltip: this.state.isSyncInProgress ?
|
||||
l10n.t('Synchronizing Changes...')
|
||||
: this.repository.syncTooltip,
|
||||
arguments: [this.repository.sourceControl],
|
||||
},
|
||||
description: `${icon}${behind}${ahead}`,
|
||||
enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress && !this.state.isCommitInProgress && !this.state.isMergeInProgress && !this.state.isRebaseInProgress && branchIsAheadOrBehind
|
||||
};
|
||||
}
|
||||
|
||||
private onDidChangeOperations(): void {
|
||||
const isCheckoutInProgress
|
||||
= this.repository.operations.isRunning(OperationKind.Checkout) ||
|
||||
this.repository.operations.isRunning(OperationKind.CheckoutTracking);
|
||||
|
||||
const isCommitInProgress =
|
||||
this.repository.operations.isRunning(OperationKind.Commit) ||
|
||||
this.repository.operations.isRunning(OperationKind.PostCommitCommand) ||
|
||||
this.repository.operations.isRunning(OperationKind.RebaseContinue);
|
||||
|
||||
const isSyncInProgress =
|
||||
this.repository.operations.isRunning(OperationKind.Sync) ||
|
||||
this.repository.operations.isRunning(OperationKind.Push) ||
|
||||
this.repository.operations.isRunning(OperationKind.Pull);
|
||||
|
||||
this.state = { ...this.state, isCheckoutInProgress, isCommitInProgress, isSyncInProgress };
|
||||
}
|
||||
|
||||
private onDidRunGitStatus(): void {
|
||||
this.state = {
|
||||
...this.state,
|
||||
HEAD: this.repository.HEAD,
|
||||
isMergeInProgress: this.repository.mergeGroup.resourceStates.length !== 0,
|
||||
isRebaseInProgress: !!this.repository.rebaseCommit,
|
||||
repositoryHasChangesToCommit: this.repositoryHasChangesToCommit()
|
||||
};
|
||||
}
|
||||
|
||||
protected repositoryHasChangesToCommit(): boolean {
|
||||
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
|
||||
const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
|
||||
const suggestSmartCommit = config.get<boolean>('suggestSmartCommit') === true;
|
||||
const smartCommitChanges = config.get<'all' | 'tracked'>('smartCommitChanges', 'all');
|
||||
|
||||
const resources = [...this.repository.indexGroup.resourceStates];
|
||||
|
||||
if (
|
||||
// Smart commit enabled (all)
|
||||
(enableSmartCommit && smartCommitChanges === 'all') ||
|
||||
// Smart commit disabled, smart suggestion enabled
|
||||
(!enableSmartCommit && suggestSmartCommit)
|
||||
) {
|
||||
resources.push(...this.repository.workingTreeGroup.resourceStates);
|
||||
}
|
||||
|
||||
// Smart commit enabled (tracked only)
|
||||
if (enableSmartCommit && smartCommitChanges === 'tracked') {
|
||||
resources.push(...this.repository.workingTreeGroup.resourceStates.filter(r => r.type !== Status.UNTRACKED));
|
||||
}
|
||||
|
||||
return resources.length !== 0;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables = dispose(this.disposables);
|
||||
}
|
||||
}
|
||||
|
||||
export class CommitActionButton extends AbstractActionButton {
|
||||
override get button(): SourceControlActionButton | undefined {
|
||||
if (!this.state.HEAD) { return undefined; }
|
||||
|
||||
let actionButton: SourceControlActionButton | undefined;
|
||||
|
||||
if (this.state.repositoryHasChangesToCommit) {
|
||||
// Commit Changes (enabled)
|
||||
actionButton = this.getCommitActionButton();
|
||||
}
|
||||
|
||||
// Commit Changes (enabled) -> Publish Branch -> Sync Changes -> Commit Changes (disabled)
|
||||
return actionButton ?? this.getPublishBranchActionButton() ?? this.getSyncChangesActionButton() ?? this.getCommitActionButton();
|
||||
}
|
||||
|
||||
constructor(
|
||||
repository: Repository,
|
||||
readonly postCommitCommandCenter: CommitCommandsCenter) {
|
||||
super(repository);
|
||||
|
||||
this.disposables.push(repository.onDidChangeBranchProtection(() => this._onDidChange.fire()));
|
||||
this.disposables.push(postCommitCommandCenter.onDidChange(() => this._onDidChange.fire()));
|
||||
|
@ -62,7 +182,8 @@ export class ActionButtonCommand {
|
|||
this.onDidChangeSmartCommitSettings();
|
||||
}
|
||||
|
||||
if (e.affectsConfiguration('git.branchProtectionPrompt', root) ||
|
||||
if (e.affectsConfiguration('scm.experimental.showSyncView') ||
|
||||
e.affectsConfiguration('git.branchProtectionPrompt', root) ||
|
||||
e.affectsConfiguration('git.postCommitCommand', root) ||
|
||||
e.affectsConfiguration('git.rememberPostCommitCommand', root) ||
|
||||
e.affectsConfiguration('git.showActionButton', root)) {
|
||||
|
@ -71,20 +192,6 @@ export class ActionButtonCommand {
|
|||
}));
|
||||
}
|
||||
|
||||
get button(): SourceControlActionButton | undefined {
|
||||
if (!this.state.HEAD) { return undefined; }
|
||||
|
||||
let actionButton: SourceControlActionButton | undefined;
|
||||
|
||||
if (this.state.repositoryHasChangesToCommit) {
|
||||
// Commit Changes (enabled)
|
||||
actionButton = this.getCommitActionButton();
|
||||
}
|
||||
|
||||
// Commit Changes (enabled) -> Publish Branch -> Sync Changes -> Commit Changes (disabled)
|
||||
return actionButton ?? this.getPublishBranchActionButton() ?? this.getSyncChangesActionButton() ?? this.getCommitActionButton();
|
||||
}
|
||||
|
||||
private getCommitActionButton(): SourceControlActionButton | undefined {
|
||||
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
|
||||
const showActionButton = config.get<{ commit: boolean }>('showActionButton', { commit: true });
|
||||
|
@ -133,34 +240,27 @@ export class ActionButtonCommand {
|
|||
return commandGroups;
|
||||
}
|
||||
|
||||
private getPublishBranchActionButton(): SourceControlActionButton | undefined {
|
||||
protected override getPublishBranchActionButton(): SourceControlActionButton | undefined {
|
||||
const scmConfig = workspace.getConfiguration('scm');
|
||||
if (scmConfig.get<boolean>('experimental.showSyncView', false)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
|
||||
const showActionButton = config.get<{ publish: boolean }>('showActionButton', { publish: true });
|
||||
|
||||
// Not a branch (tag, detached), branch does have an upstream, commit/merge/rebase is in progress, or the button is disabled
|
||||
if (this.state.HEAD?.type === RefType.Tag || !this.state.HEAD?.name || this.state.HEAD?.upstream || this.state.isCommitInProgress || this.state.isMergeInProgress || this.state.isRebaseInProgress || !showActionButton.publish) { return undefined; }
|
||||
|
||||
// Button icon
|
||||
const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(cloud-upload)';
|
||||
|
||||
return {
|
||||
command: {
|
||||
command: 'git.publish',
|
||||
title: l10n.t({ message: '{0} Publish Branch', args: [icon], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }),
|
||||
tooltip: this.state.isSyncInProgress ?
|
||||
(this.state.HEAD?.name ?
|
||||
l10n.t({ message: 'Publishing Branch "{0}"...', args: [this.state.HEAD.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) :
|
||||
l10n.t({ message: 'Publishing Branch...', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })) :
|
||||
(this.repository.HEAD?.name ?
|
||||
l10n.t({ message: 'Publish Branch "{0}"', args: [this.state.HEAD?.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) :
|
||||
l10n.t({ message: 'Publish Branch', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })),
|
||||
arguments: [this.repository.sourceControl],
|
||||
},
|
||||
enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress
|
||||
};
|
||||
return super.getPublishBranchActionButton();
|
||||
}
|
||||
|
||||
private getSyncChangesActionButton(): SourceControlActionButton | undefined {
|
||||
protected override getSyncChangesActionButton(): SourceControlActionButton | undefined {
|
||||
const scmConfig = workspace.getConfiguration('scm');
|
||||
if (scmConfig.get<boolean>('experimental.showSyncView', false)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
|
||||
const showActionButton = config.get<{ sync: boolean }>('showActionButton', { sync: true });
|
||||
const branchIsAheadOrBehind = (this.state.HEAD?.behind ?? 0) > 0 || (this.state.HEAD?.ahead ?? 0) > 0;
|
||||
|
@ -168,40 +268,7 @@ export class ActionButtonCommand {
|
|||
// Branch does not have an upstream, branch is not ahead/behind the remote branch, commit/merge/rebase is in progress, or the button is disabled
|
||||
if (!this.state.HEAD?.upstream || !branchIsAheadOrBehind || this.state.isCommitInProgress || this.state.isMergeInProgress || this.state.isRebaseInProgress || !showActionButton.sync) { return undefined; }
|
||||
|
||||
const ahead = this.state.HEAD.ahead ? ` ${this.state.HEAD.ahead}$(arrow-up)` : '';
|
||||
const behind = this.state.HEAD.behind ? ` ${this.state.HEAD.behind}$(arrow-down)` : '';
|
||||
const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(sync)';
|
||||
|
||||
return {
|
||||
command: {
|
||||
command: 'git.sync',
|
||||
title: l10n.t('{0} Sync Changes{1}{2}', icon, behind, ahead),
|
||||
tooltip: this.state.isSyncInProgress ?
|
||||
l10n.t('Synchronizing Changes...')
|
||||
: this.repository.syncTooltip,
|
||||
arguments: [this.repository.sourceControl],
|
||||
},
|
||||
description: `${icon}${behind}${ahead}`,
|
||||
enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress
|
||||
};
|
||||
}
|
||||
|
||||
private onDidChangeOperations(): void {
|
||||
const isCheckoutInProgress
|
||||
= this.repository.operations.isRunning(OperationKind.Checkout) ||
|
||||
this.repository.operations.isRunning(OperationKind.CheckoutTracking);
|
||||
|
||||
const isCommitInProgress =
|
||||
this.repository.operations.isRunning(OperationKind.Commit) ||
|
||||
this.repository.operations.isRunning(OperationKind.PostCommitCommand) ||
|
||||
this.repository.operations.isRunning(OperationKind.RebaseContinue);
|
||||
|
||||
const isSyncInProgress =
|
||||
this.repository.operations.isRunning(OperationKind.Sync) ||
|
||||
this.repository.operations.isRunning(OperationKind.Push) ||
|
||||
this.repository.operations.isRunning(OperationKind.Pull);
|
||||
|
||||
this.state = { ...this.state, isCheckoutInProgress, isCommitInProgress, isSyncInProgress };
|
||||
return super.getSyncChangesActionButton();
|
||||
}
|
||||
|
||||
private onDidChangeSmartCommitSettings(): void {
|
||||
|
@ -210,43 +277,30 @@ export class ActionButtonCommand {
|
|||
repositoryHasChangesToCommit: this.repositoryHasChangesToCommit()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private onDidRunGitStatus(): void {
|
||||
this.state = {
|
||||
...this.state,
|
||||
HEAD: this.repository.HEAD,
|
||||
isMergeInProgress: this.repository.mergeGroup.resourceStates.length !== 0,
|
||||
isRebaseInProgress: !!this.repository.rebaseCommit,
|
||||
repositoryHasChangesToCommit: this.repositoryHasChangesToCommit()
|
||||
};
|
||||
export class SyncActionButton extends AbstractActionButton {
|
||||
override get button(): SourceControlActionButton | undefined {
|
||||
if (!this.state.HEAD) { return undefined; }
|
||||
|
||||
// Publish Branch -> Sync Changes
|
||||
return this.getPublishBranchActionButton() ?? this.getSyncChangesActionButton();
|
||||
}
|
||||
|
||||
private repositoryHasChangesToCommit(): boolean {
|
||||
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
|
||||
const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
|
||||
const suggestSmartCommit = config.get<boolean>('suggestSmartCommit') === true;
|
||||
const smartCommitChanges = config.get<'all' | 'tracked'>('smartCommitChanges', 'all');
|
||||
constructor(repository: Repository) {
|
||||
super(repository);
|
||||
|
||||
const resources = [...this.repository.indexGroup.resourceStates];
|
||||
|
||||
if (
|
||||
// Smart commit enabled (all)
|
||||
(enableSmartCommit && smartCommitChanges === 'all') ||
|
||||
// Smart commit disabled, smart suggestion enabled
|
||||
(!enableSmartCommit && suggestSmartCommit)
|
||||
) {
|
||||
resources.push(...this.repository.workingTreeGroup.resourceStates);
|
||||
}
|
||||
|
||||
// Smart commit enabled (tracked only)
|
||||
if (enableSmartCommit && smartCommitChanges === 'tracked') {
|
||||
resources.push(...this.repository.workingTreeGroup.resourceStates.filter(r => r.type !== Status.UNTRACKED));
|
||||
}
|
||||
|
||||
return resources.length !== 0;
|
||||
this.disposables.push(workspace.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('scm.experimental.showSyncView')) {
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables = dispose(this.disposables);
|
||||
protected override getPublishBranchActionButton(): SourceControlActionButton | undefined {
|
||||
// Not a branch (tag, detached), branch does have an upstream
|
||||
if (this.state.HEAD?.type === RefType.Tag || this.state.HEAD?.upstream) { return undefined; }
|
||||
|
||||
return super.getPublishBranchActionButton();
|
||||
}
|
||||
}
|
||||
|
|
2
extensions/git/src/api/git.d.ts
vendored
2
extensions/git/src/api/git.d.ts
vendored
|
@ -131,6 +131,8 @@ export interface LogOptions {
|
|||
readonly path?: string;
|
||||
/** A commit range, such as "0a47c67f0fb52dd11562af48658bc1dff1d75a38..0bb4bdea78e1db44d728fd6894720071e303304f" */
|
||||
readonly range?: string;
|
||||
readonly reverse?: boolean;
|
||||
readonly sortByAuthorDate?: boolean;
|
||||
}
|
||||
|
||||
export interface CommitOptions {
|
||||
|
|
|
@ -1023,17 +1023,24 @@ export class Repository {
|
|||
}
|
||||
|
||||
async log(options?: LogOptions): Promise<Commit[]> {
|
||||
const maxEntries = options?.maxEntries ?? 32;
|
||||
const args = ['log', `-n${maxEntries}`, `--format=${COMMIT_FORMAT}`, '-z'];
|
||||
const args = ['log', `--format=${COMMIT_FORMAT}`, '-z'];
|
||||
|
||||
if (options?.reverse) {
|
||||
args.push('--reverse', '--ancestry-path');
|
||||
}
|
||||
|
||||
if (options?.sortByAuthorDate) {
|
||||
args.push('--author-date-order');
|
||||
}
|
||||
|
||||
if (options?.range) {
|
||||
args.push(options.range);
|
||||
} else {
|
||||
args.push(`-n${options?.maxEntries ?? 32}`);
|
||||
}
|
||||
|
||||
args.push('--');
|
||||
|
||||
if (options?.path) {
|
||||
args.push(options.path);
|
||||
args.push('--', options.path);
|
||||
}
|
||||
|
||||
const result = await this.exec(args);
|
||||
|
@ -1258,7 +1265,7 @@ export class Repository {
|
|||
|
||||
diffIndexWithHEAD(): Promise<Change[]>;
|
||||
diffIndexWithHEAD(path: string): Promise<string>;
|
||||
diffIndexWithHEAD(path?: string | undefined): Promise<string | Change[]>;
|
||||
diffIndexWithHEAD(path?: string | undefined): Promise<Change[]>;
|
||||
async diffIndexWithHEAD(path?: string): Promise<string | Change[]> {
|
||||
if (!path) {
|
||||
return await this.diffFiles(true);
|
||||
|
@ -1303,6 +1310,17 @@ export class Repository {
|
|||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
async diffBetweenShortStat(ref1: string, ref2: string): Promise<string> {
|
||||
const args = ['diff', '--shortstat', `${ref1}...${ref2}`];
|
||||
|
||||
const result = await this.exec(args);
|
||||
if (result.exitCode) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
private async diffFiles(cached: boolean, ref?: string): Promise<Change[]> {
|
||||
const args = ['diff', '--name-status', '-z', '--diff-filter=ADMR'];
|
||||
if (cached) {
|
||||
|
@ -2450,6 +2468,15 @@ export class Repository {
|
|||
return Promise.reject<Branch>(new Error('No such branch'));
|
||||
}
|
||||
|
||||
async getDefaultBranch(): Promise<Branch> {
|
||||
const result = await this.exec(['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);
|
||||
if (!result.stdout) {
|
||||
throw new Error('No default branch');
|
||||
}
|
||||
|
||||
return this.getBranch(result.stdout.trim());
|
||||
}
|
||||
|
||||
// TODO: Support core.commentChar
|
||||
stripCommitMessageComments(message: string): string {
|
||||
return message.replace(/^\s*#.*$\n?/gm, '').trim();
|
||||
|
@ -2510,6 +2537,13 @@ export class Repository {
|
|||
return commits[0];
|
||||
}
|
||||
|
||||
async getCommitCount(range: string): Promise<{ ahead: number; behind: number }> {
|
||||
const result = await this.exec(['rev-list', '--count', '--left-right', range]);
|
||||
const [ahead, behind] = result.stdout.trim().split('\t');
|
||||
|
||||
return { ahead: Number(ahead) || 0, behind: Number(behind) || 0 };
|
||||
}
|
||||
|
||||
async updateSubmodules(paths: string[]): Promise<void> {
|
||||
const args = ['submodule', 'update'];
|
||||
|
||||
|
|
132
extensions/git/src/historyProvider.ts
Normal file
132
extensions/git/src/historyProvider.ts
Normal file
|
@ -0,0 +1,132 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { Disposable, Event, EventEmitter, SourceControlActionButton, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon } from 'vscode';
|
||||
import { Repository } from './repository';
|
||||
import { IDisposable } from './util';
|
||||
import { toGitUri } from './uri';
|
||||
import { SyncActionButton } from './actionButton';
|
||||
|
||||
export class GitHistoryProvider implements SourceControlHistoryProvider, IDisposable {
|
||||
|
||||
private readonly _onDidChangeActionButton = new EventEmitter<void>();
|
||||
readonly onDidChangeActionButton: Event<void> = this._onDidChangeActionButton.event;
|
||||
|
||||
private readonly _onDidChangeCurrentHistoryItemGroup = new EventEmitter<void>();
|
||||
readonly onDidChangeCurrentHistoryItemGroup: Event<void> = this._onDidChangeCurrentHistoryItemGroup.event;
|
||||
|
||||
private _actionButton: SourceControlActionButton | undefined;
|
||||
get actionButton(): SourceControlActionButton | undefined { return this._actionButton; }
|
||||
set actionButton(button: SourceControlActionButton | undefined) {
|
||||
this._actionButton = button;
|
||||
this._onDidChangeActionButton.fire();
|
||||
}
|
||||
|
||||
private _currentHistoryItemGroup: SourceControlHistoryItemGroup | undefined;
|
||||
|
||||
get currentHistoryItemGroup(): SourceControlHistoryItemGroup | undefined { return this._currentHistoryItemGroup; }
|
||||
set currentHistoryItemGroup(value: SourceControlHistoryItemGroup | undefined) {
|
||||
this._currentHistoryItemGroup = value;
|
||||
this._onDidChangeCurrentHistoryItemGroup.fire();
|
||||
}
|
||||
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(protected readonly repository: Repository) {
|
||||
const actionButton = new SyncActionButton(repository);
|
||||
this.actionButton = actionButton.button;
|
||||
this.disposables.push(actionButton);
|
||||
|
||||
this.disposables.push(repository.onDidRunGitStatus(this.onDidRunGitStatus, this));
|
||||
this.disposables.push(actionButton.onDidChange(() => this.actionButton = actionButton.button));
|
||||
}
|
||||
|
||||
private async onDidRunGitStatus(): Promise<void> {
|
||||
if (!this.repository.HEAD?.name || !this.repository.HEAD?.commit) { return; }
|
||||
|
||||
this.currentHistoryItemGroup = {
|
||||
id: `refs/heads/${this.repository.HEAD.name}`,
|
||||
label: this.repository.HEAD.name,
|
||||
upstream: this.repository.HEAD.upstream ?
|
||||
{
|
||||
id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`,
|
||||
label: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`,
|
||||
} : undefined
|
||||
};
|
||||
}
|
||||
|
||||
async provideHistoryItems(historyItemGroupId: string, options: SourceControlHistoryOptions): Promise<SourceControlHistoryItem[]> {
|
||||
//TODO@lszomoru - support limit and cursor
|
||||
if (typeof options.limit === 'number') {
|
||||
throw new Error('Unsupported options.');
|
||||
}
|
||||
if (typeof options.limit?.id !== 'string') {
|
||||
throw new Error('Unsupported options.');
|
||||
}
|
||||
|
||||
const optionsRef = options.limit.id;
|
||||
const [commits, summary] = await Promise.all([
|
||||
this.repository.log({ range: `${optionsRef}..${historyItemGroupId}`, sortByAuthorDate: true }),
|
||||
this.getSummaryHistoryItem(optionsRef, historyItemGroupId)
|
||||
]);
|
||||
|
||||
const historyItems = commits.length === 0 ? [] : [summary];
|
||||
historyItems.push(...commits.map(commit => {
|
||||
const newLineIndex = commit.message.indexOf('\n');
|
||||
const subject = newLineIndex !== -1 ? commit.message.substring(0, newLineIndex) : commit.message;
|
||||
|
||||
return {
|
||||
id: commit.hash,
|
||||
parentIds: commit.parents,
|
||||
label: subject,
|
||||
description: commit.authorName,
|
||||
icon: new ThemeIcon('account'),
|
||||
timestamp: commit.authorDate?.getTime()
|
||||
};
|
||||
}));
|
||||
|
||||
return historyItems;
|
||||
}
|
||||
|
||||
async provideHistoryItemChanges(historyItemId: string): Promise<SourceControlHistoryItemChange[]> {
|
||||
const [ref1, ref2] = historyItemId.includes('..')
|
||||
? historyItemId.split('..')
|
||||
: [`${historyItemId}^`, historyItemId];
|
||||
|
||||
const changes = await this.repository.diffBetween(ref1, ref2);
|
||||
|
||||
return changes.map(change => ({
|
||||
uri: change.uri.with({ query: `ref=${historyItemId}` }),
|
||||
originalUri: toGitUri(change.originalUri, ref1),
|
||||
modifiedUri: toGitUri(change.originalUri, ref2),
|
||||
renameUri: change.renameUri,
|
||||
}));
|
||||
}
|
||||
|
||||
async resolveHistoryItemGroupCommonAncestor(refId1: string, refId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined> {
|
||||
refId2 = refId2 ?? (await this.repository.getDefaultBranch()).name ?? '';
|
||||
if (refId2 === '') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const ancestor = await this.repository.getMergeBase(refId1, refId2);
|
||||
if (ancestor === '') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const commitCount = await this.repository.getCommitCount(`${refId1}...${refId2}`);
|
||||
return { id: ancestor, ahead: commitCount.ahead, behind: commitCount.behind };
|
||||
}
|
||||
|
||||
private async getSummaryHistoryItem(ref1: string, ref2: string): Promise<SourceControlHistoryItem> {
|
||||
const diffShortStat = await this.repository.diffBetweenShortStat(ref1, ref2);
|
||||
return { id: `${ref1}..${ref2}`, parentIds: [], icon: new ThemeIcon('files'), label: 'Changes', description: diffShortStat };
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
|
@ -53,6 +53,7 @@ export const enum OperationKind {
|
|||
RebaseContinue = 'RebaseContinue',
|
||||
RevertFiles = 'RevertFiles',
|
||||
RevertFilesNoProgress = 'RevertFilesNoProgress',
|
||||
RevList = 'RevList',
|
||||
SetBranchUpstream = 'SetBranchUpstream',
|
||||
Show = 'Show',
|
||||
Stage = 'Stage',
|
||||
|
@ -69,8 +70,9 @@ export type Operation = AddOperation | ApplyOperation | BlameOperation | BranchO
|
|||
GetBranchOperation | GetBranchesOperation | GetCommitTemplateOperation | GetObjectDetailsOperation | GetRefsOperation | GetRemoteRefsOperation |
|
||||
HashObjectOperation | IgnoreOperation | LogOperation | LogFileOperation | MergeOperation | MergeAbortOperation | MergeBaseOperation |
|
||||
MoveOperation | PostCommitCommandOperation | PullOperation | PushOperation | RemoteOperation | RenameBranchOperation | RemoveOperation |
|
||||
ResetOperation | RebaseOperation | RebaseAbortOperation | RebaseContinueOperation | RevertFilesOperation | SetBranchUpstreamOperation |
|
||||
ShowOperation | StageOperation | StatusOperation | StashOperation | SubmoduleUpdateOperation | SyncOperation | TagOperation;
|
||||
ResetOperation | RebaseOperation | RebaseAbortOperation | RebaseContinueOperation | RevertFilesOperation | RevListOperation |
|
||||
SetBranchUpstreamOperation | ShowOperation | StageOperation | StatusOperation | StashOperation | SubmoduleUpdateOperation | SyncOperation |
|
||||
TagOperation;
|
||||
|
||||
type BaseOperation = { kind: OperationKind; blocking: boolean; readOnly: boolean; remote: boolean; retry: boolean; showProgress: boolean };
|
||||
export type AddOperation = BaseOperation & { kind: OperationKind.Add };
|
||||
|
@ -116,6 +118,7 @@ export type RebaseOperation = BaseOperation & { kind: OperationKind.Rebase };
|
|||
export type RebaseAbortOperation = BaseOperation & { kind: OperationKind.RebaseAbort };
|
||||
export type RebaseContinueOperation = BaseOperation & { kind: OperationKind.RebaseContinue };
|
||||
export type RevertFilesOperation = BaseOperation & { kind: OperationKind.RevertFiles };
|
||||
export type RevListOperation = BaseOperation & { kind: OperationKind.RevList };
|
||||
export type SetBranchUpstreamOperation = BaseOperation & { kind: OperationKind.SetBranchUpstream };
|
||||
export type ShowOperation = BaseOperation & { kind: OperationKind.Show };
|
||||
export type StageOperation = BaseOperation & { kind: OperationKind.Stage };
|
||||
|
@ -169,6 +172,7 @@ export const Operation = {
|
|||
RebaseAbort: { kind: OperationKind.RebaseAbort, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RebaseAbortOperation,
|
||||
RebaseContinue: { kind: OperationKind.RebaseContinue, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RebaseContinueOperation,
|
||||
RevertFiles: (showProgress: boolean) => ({ kind: OperationKind.RevertFiles, blocking: false, readOnly: false, remote: false, retry: false, showProgress } as RevertFilesOperation),
|
||||
RevList: { kind: OperationKind.RevList, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as RevListOperation,
|
||||
SetBranchUpstream: { kind: OperationKind.SetBranchUpstream, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as SetBranchUpstreamOperation,
|
||||
Show: { kind: OperationKind.Show, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as ShowOperation,
|
||||
Stage: { kind: OperationKind.Stage, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as StageOperation,
|
||||
|
|
|
@ -19,10 +19,11 @@ import { IFileWatcher, watch } from './watch';
|
|||
import { IPushErrorHandlerRegistry } from './pushError';
|
||||
import { ApiRepository } from './api/api1';
|
||||
import { IRemoteSourcePublisherRegistry } from './remotePublisher';
|
||||
import { ActionButtonCommand } from './actionButton';
|
||||
import { CommitActionButton } from './actionButton';
|
||||
import { IPostCommitCommandsProviderRegistry, CommitCommandsCenter } from './postCommitCommands';
|
||||
import { Operation, OperationKind, OperationManager, OperationResult } from './operation';
|
||||
import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection';
|
||||
import { GitHistoryProvider } from './historyProvider';
|
||||
|
||||
const timeout = (millis: number) => new Promise(c => setTimeout(c, millis));
|
||||
|
||||
|
@ -833,8 +834,13 @@ export class Repository implements Disposable {
|
|||
const root = Uri.file(repository.root);
|
||||
this._sourceControl = scm.createSourceControl('git', 'Git', root);
|
||||
|
||||
this._sourceControl.acceptInputCommand = { command: 'git.commit', title: l10n.t('Commit'), arguments: [this._sourceControl] };
|
||||
this._sourceControl.quickDiffProvider = this;
|
||||
|
||||
const historyProvider = new GitHistoryProvider(this);
|
||||
this._sourceControl.historyProvider = historyProvider;
|
||||
this.disposables.push(historyProvider);
|
||||
|
||||
this._sourceControl.acceptInputCommand = { command: 'git.commit', title: l10n.t('Commit'), arguments: [this._sourceControl] };
|
||||
this._sourceControl.inputBox.validateInput = this.validateInput.bind(this);
|
||||
this.disposables.push(this._sourceControl);
|
||||
|
||||
|
@ -921,10 +927,10 @@ export class Repository implements Disposable {
|
|||
this.commitCommandCenter = new CommitCommandsCenter(globalState, this, postCommitCommandsProviderRegistry);
|
||||
this.disposables.push(this.commitCommandCenter);
|
||||
|
||||
const actionButton = new ActionButtonCommand(this, this.commitCommandCenter);
|
||||
this.disposables.push(actionButton);
|
||||
actionButton.onDidChange(() => this._sourceControl.actionButton = actionButton.button);
|
||||
this._sourceControl.actionButton = actionButton.button;
|
||||
const commitActionButton = new CommitActionButton(this, this.commitCommandCenter);
|
||||
this.disposables.push(commitActionButton);
|
||||
commitActionButton.onDidChange(() => this._sourceControl.actionButton = commitActionButton.button);
|
||||
this._sourceControl.actionButton = commitActionButton.button;
|
||||
|
||||
const progressManager = new ProgressManager(this);
|
||||
this.disposables.push(progressManager);
|
||||
|
@ -1115,6 +1121,10 @@ export class Repository implements Disposable {
|
|||
return this.run(Operation.Diff, () => this.repository.diffBetween(ref1, ref2, path));
|
||||
}
|
||||
|
||||
diffBetweenShortStat(ref1: string, ref2: string): Promise<string> {
|
||||
return this.run(Operation.Diff, () => this.repository.diffBetweenShortStat(ref1, ref2));
|
||||
}
|
||||
|
||||
getMergeBase(ref1: string, ref2: string): Promise<string> {
|
||||
return this.run(Operation.MergeBase, () => this.repository.getMergeBase(ref1, ref2));
|
||||
}
|
||||
|
@ -1421,6 +1431,10 @@ export class Repository implements Disposable {
|
|||
await this.run(Operation.Move, () => this.repository.move(from, to));
|
||||
}
|
||||
|
||||
async getDefaultBranch(): Promise<Branch> {
|
||||
return await this.run(Operation.GetBranch, () => this.repository.getDefaultBranch());
|
||||
}
|
||||
|
||||
async getBranch(name: string): Promise<Branch> {
|
||||
return await this.run(Operation.GetBranch, () => this.repository.getBranch(name));
|
||||
}
|
||||
|
@ -1506,6 +1520,10 @@ export class Repository implements Disposable {
|
|||
return await this.repository.getCommit(ref);
|
||||
}
|
||||
|
||||
async getCommitCount(range: string): Promise<{ ahead: number; behind: number }> {
|
||||
return await this.run(Operation.RevList, () => this.repository.getCommitCount(range));
|
||||
}
|
||||
|
||||
async reset(treeish: string, hard?: boolean): Promise<void> {
|
||||
await this.run(Operation.Reset, () => this.repository.reset(treeish, hard));
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"../../src/vscode-dts/vscode.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.diffCommand.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.scmActionButton.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.scmValidation.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts",
|
||||
|
|
|
@ -7,7 +7,7 @@ import { URI, UriComponents } from 'vs/base/common/uri';
|
|||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IDisposable, DisposableStore, combinedDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService, InputValidationType, ISCMActionButtonDescriptor } from 'vs/workbench/contrib/scm/common/scm';
|
||||
import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext } from '../common/extHost.protocol';
|
||||
import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemDto, SCMActionButtonDto, SCMHistoryItemGroupDto } from '../common/extHost.protocol';
|
||||
import { Command } from 'vs/editor/common/languages';
|
||||
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
|
||||
import { ISplice, Sequence } from 'vs/base/common/sequence';
|
||||
|
@ -16,6 +16,20 @@ import { MarshalledId } from 'vs/base/common/marshallingIds';
|
|||
import { ThemeIcon } from 'vs/base/common/themables';
|
||||
import { IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { IQuickDiffService, QuickDiffProvider } from 'vs/workbench/contrib/scm/common/quickDiff';
|
||||
import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryOptions, ISCMHistoryProvider } from 'vs/workbench/contrib/scm/common/history';
|
||||
|
||||
function getSCMHistoryItemIcon(historyItem: SCMHistoryItemDto): URI | { light: URI; dark: URI } | ThemeIcon | undefined {
|
||||
if (!historyItem.icon) {
|
||||
return undefined;
|
||||
} else if (URI.isUri(historyItem.icon)) {
|
||||
return URI.revive(historyItem.icon);
|
||||
} else if (ThemeIcon.isThemeIcon(historyItem.icon)) {
|
||||
return historyItem.icon;
|
||||
} else {
|
||||
const icon = historyItem.icon as { light: UriComponents; dark: UriComponents };
|
||||
return { light: URI.revive(icon.light), dark: URI.revive(icon.dark) };
|
||||
}
|
||||
}
|
||||
|
||||
class MainThreadSCMResourceGroup implements ISCMResourceGroup {
|
||||
|
||||
|
@ -121,6 +135,7 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider {
|
|||
get contextValue(): string { return this._contextValue; }
|
||||
|
||||
get commitTemplate(): string { return this.features.commitTemplate || ''; }
|
||||
get historyProvider(): ISCMHistoryProvider | undefined { return this._historyProvider; }
|
||||
get acceptInputCommand(): Command | undefined { return this.features.acceptInputCommand; }
|
||||
get actionButton(): ISCMActionButtonDescriptor | undefined { return this.features.actionButton ?? undefined; }
|
||||
get statusBarCommands(): Command[] | undefined { return this.features.statusBarCommands; }
|
||||
|
@ -132,12 +147,22 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider {
|
|||
private readonly _onDidChangeStatusBarCommands = new Emitter<readonly Command[]>();
|
||||
get onDidChangeStatusBarCommands(): Event<readonly Command[]> { return this._onDidChangeStatusBarCommands.event; }
|
||||
|
||||
private readonly _onDidChangeHistoryProviderActionButton = new Emitter<void>();
|
||||
readonly onDidChangeHistoryProviderActionButton: Event<void> = this._onDidChangeHistoryProviderActionButton.event;
|
||||
|
||||
private readonly _onDidChangeHistoryProviderCurrentHistoryItemGroup = new Emitter<void>();
|
||||
readonly onDidChangeHistoryProviderCurrentHistoryItemGroup: Event<void> = this._onDidChangeHistoryProviderCurrentHistoryItemGroup.event;
|
||||
|
||||
private readonly _onDidChange = new Emitter<void>();
|
||||
readonly onDidChange: Event<void> = this._onDidChange.event;
|
||||
|
||||
private _quickDiff: IDisposable | undefined;
|
||||
public readonly isSCM: boolean = true;
|
||||
|
||||
private _historyProvider: ISCMHistoryProvider | undefined;
|
||||
private _historyProviderActionButton: SCMActionButtonDto | undefined | null;
|
||||
private _historyProviderCurrentHistoryItemGroup: SCMHistoryItemGroupDto | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly proxy: ExtHostSCMShape,
|
||||
private readonly _handle: number,
|
||||
|
@ -280,6 +305,45 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider {
|
|||
return result && URI.revive(result);
|
||||
}
|
||||
|
||||
$registerHistoryProvider(): void {
|
||||
this._historyProvider = {
|
||||
actionButton: () => this._historyProviderActionButton ?? undefined,
|
||||
currentHistoryItemGroup: () => this._historyProviderCurrentHistoryItemGroup ?? undefined,
|
||||
provideHistoryItems: (historyItemGroupId, options) => this.provideHistoryItems(historyItemGroupId, options),
|
||||
provideHistoryItemChanges: (historyItemId: string) => this.provideHistoryItemChanges(historyItemId),
|
||||
resolveHistoryItemGroupCommonAncestor: (historyItemGroupId1, historyItemGroupId2) => this.resolveHistoryItemGroupCommonAncestor(historyItemGroupId1, historyItemGroupId2),
|
||||
};
|
||||
}
|
||||
|
||||
$onDidChangeHistoryProviderActionButton(actionButton?: SCMActionButtonDto | null): void {
|
||||
this._historyProviderActionButton = actionButton;
|
||||
this._onDidChangeHistoryProviderActionButton.fire();
|
||||
}
|
||||
|
||||
$onDidChangeHistoryProviderCurrentHistoryItemGroup(currentHistoryItemGroup?: SCMHistoryItemGroupDto): void {
|
||||
this._historyProviderCurrentHistoryItemGroup = currentHistoryItemGroup;
|
||||
this._onDidChangeHistoryProviderCurrentHistoryItemGroup.fire();
|
||||
}
|
||||
|
||||
async resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined> {
|
||||
return this.proxy.$resolveHistoryItemGroupCommonAncestor(this.handle, historyItemGroupId1, historyItemGroupId2, CancellationToken.None);
|
||||
}
|
||||
|
||||
async provideHistoryItems(historyItemGroupId: string, options: ISCMHistoryOptions): Promise<ISCMHistoryItem[] | undefined> {
|
||||
const historyItems = await this.proxy.$provideHistoryItems(this.handle, historyItemGroupId, options, CancellationToken.None);
|
||||
return historyItems?.map(historyItem => ({ ...historyItem, icon: getSCMHistoryItemIcon(historyItem), }));
|
||||
}
|
||||
|
||||
async provideHistoryItemChanges(historyItemId: string): Promise<ISCMHistoryItemChange[] | undefined> {
|
||||
const changes = await this.proxy.$provideHistoryItemChanges(this.handle, historyItemId, CancellationToken.None);
|
||||
return changes?.map(change => ({
|
||||
uri: URI.revive(change.uri),
|
||||
originalUri: change.originalUri && URI.revive(change.originalUri),
|
||||
modifiedUri: change.modifiedUri && URI.revive(change.modifiedUri),
|
||||
renameUri: change.renameUri && URI.revive(change.renameUri)
|
||||
}));
|
||||
}
|
||||
|
||||
toJSON(): any {
|
||||
return {
|
||||
$mid: MarshalledId.ScmProvider,
|
||||
|
@ -486,4 +550,37 @@ export class MainThreadSCM implements MainThreadSCMShape {
|
|||
repository.input.validateInput = async () => undefined;
|
||||
}
|
||||
}
|
||||
|
||||
$registerHistoryProvider(sourceControlHandle: number): void {
|
||||
const repository = this._repositories.get(sourceControlHandle);
|
||||
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = repository.provider as MainThreadSCMProvider;
|
||||
provider.$registerHistoryProvider();
|
||||
}
|
||||
|
||||
$onDidChangeHistoryProviderActionButton(sourceControlHandle: number, actionButton?: SCMActionButtonDto | null | undefined): void {
|
||||
const repository = this._repositories.get(sourceControlHandle);
|
||||
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = repository.provider as MainThreadSCMProvider;
|
||||
provider.$onDidChangeHistoryProviderActionButton(actionButton);
|
||||
}
|
||||
|
||||
$onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): void {
|
||||
const repository = this._repositories.get(sourceControlHandle);
|
||||
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = repository.provider as MainThreadSCMProvider;
|
||||
provider.$onDidChangeHistoryProviderCurrentHistoryItemGroup(historyItemGroup);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1401,6 +1401,28 @@ export type SCMRawResourceSplices = [
|
|||
SCMRawResourceSplice[]
|
||||
];
|
||||
|
||||
export interface SCMHistoryItemGroupDto {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly upstream?: SCMHistoryItemGroupDto;
|
||||
}
|
||||
|
||||
export interface SCMHistoryItemDto {
|
||||
readonly id: string;
|
||||
readonly parentIds: string[];
|
||||
readonly label: string;
|
||||
readonly description?: string;
|
||||
readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon;
|
||||
readonly timestamp?: number;
|
||||
}
|
||||
|
||||
export interface SCMHistoryItemChangeDto {
|
||||
readonly uri: UriComponents;
|
||||
readonly originalUri: UriComponents | undefined;
|
||||
readonly modifiedUri: UriComponents | undefined;
|
||||
readonly renameUri: UriComponents | undefined;
|
||||
}
|
||||
|
||||
export interface MainThreadSCMShape extends IDisposable {
|
||||
$registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): void;
|
||||
$updateSourceControl(handle: number, features: SCMProviderFeatures): void;
|
||||
|
@ -1419,6 +1441,10 @@ export interface MainThreadSCMShape extends IDisposable {
|
|||
$setInputBoxVisibility(sourceControlHandle: number, visible: boolean): void;
|
||||
$showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType): void;
|
||||
$setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): void;
|
||||
|
||||
$registerHistoryProvider(sourceControlHandle: number): void;
|
||||
$onDidChangeHistoryProviderActionButton(sourceControlHandle: number, actionButton?: SCMActionButtonDto | null): void;
|
||||
$onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): void;
|
||||
}
|
||||
|
||||
export interface MainThreadQuickDiffShape extends IDisposable {
|
||||
|
@ -2126,6 +2152,9 @@ export interface ExtHostSCMShape {
|
|||
$executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number, preserveFocus: boolean): Promise<void>;
|
||||
$validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Promise<[string | IMarkdownString, number] | undefined>;
|
||||
$setSelectedSourceControl(selectedSourceControlHandle: number | undefined): Promise<void>;
|
||||
$provideHistoryItems(sourceControlHandle: number, historyItemGroupId: string, options: any, token: CancellationToken): Promise<SCMHistoryItemDto[] | undefined>;
|
||||
$provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, token: CancellationToken): Promise<SCMHistoryItemChangeDto[] | undefined>;
|
||||
$resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupId1: string, historyItemGroupId2: string | undefined, token: CancellationToken): Promise<{ id: string; ahead: number; behind: number } | undefined>;
|
||||
}
|
||||
|
||||
export interface ExtHostQuickDiffShape {
|
||||
|
|
|
@ -11,7 +11,7 @@ import { debounce } from 'vs/base/common/decorators';
|
|||
import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||
import { asPromise } from 'vs/base/common/async';
|
||||
import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
|
||||
import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures } from './extHost.protocol';
|
||||
import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures, SCMHistoryItemDto, SCMHistoryItemChangeDto } from './extHost.protocol';
|
||||
import { sortedDiff, equals } from 'vs/base/common/arrays';
|
||||
import { comparePaths } from 'vs/base/common/comparers';
|
||||
import type * as vscode from 'vscode';
|
||||
|
@ -45,6 +45,19 @@ function getIconResource(decorations?: vscode.SourceControlResourceThemableDecor
|
|||
}
|
||||
}
|
||||
|
||||
function getHistoryItemIconDto(historyItem: vscode.SourceControlHistoryItem): UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon | undefined {
|
||||
if (!historyItem.icon) {
|
||||
return undefined;
|
||||
} else if (URI.isUri(historyItem.icon)) {
|
||||
return historyItem.icon;
|
||||
} else if (ThemeIcon.isThemeIcon(historyItem.icon)) {
|
||||
return historyItem.icon;
|
||||
} else {
|
||||
const icon = historyItem.icon as { light: URI; dark: URI };
|
||||
return { light: icon.light, dark: icon.dark };
|
||||
}
|
||||
}
|
||||
|
||||
function compareResourceThemableDecorations(a: vscode.SourceControlResourceThemableDecorations, b: vscode.SourceControlResourceThemableDecorations): number {
|
||||
if (!a.iconPath && !b.iconPath) {
|
||||
return 0;
|
||||
|
@ -197,6 +210,18 @@ function commandListEquals(a: readonly vscode.Command[], b: readonly vscode.Comm
|
|||
return equals(a, b, commandEquals);
|
||||
}
|
||||
|
||||
function historyItemGroupEquals(a: vscode.SourceControlHistoryItemGroup | undefined, b: vscode.SourceControlHistoryItemGroup | undefined): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!a || !b) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return a.id === b.id && a.label === b.label && a.upstream?.id === b.upstream?.id && a.upstream?.label === b.upstream?.label;
|
||||
}
|
||||
|
||||
export interface IValidateInput {
|
||||
(value: string, cursorPosition: number): vscode.ProviderResult<vscode.SourceControlInputBoxValidation | undefined | null>;
|
||||
}
|
||||
|
@ -507,6 +532,52 @@ class ExtHostSourceControl implements vscode.SourceControl {
|
|||
this.#proxy.$updateSourceControl(this.handle, { hasQuickDiffProvider: !!quickDiffProvider, quickDiffLabel });
|
||||
}
|
||||
|
||||
private _historyProvider: vscode.SourceControlHistoryProvider | undefined;
|
||||
private _historyProviderDisposable = new MutableDisposable<DisposableStore>();
|
||||
private _historyProviderCurrentHistoryItemGroup: vscode.SourceControlHistoryItemGroup | undefined;
|
||||
private _historyProviderActionButtonDisposable = new MutableDisposable<DisposableStore>();
|
||||
|
||||
get historyProvider(): vscode.SourceControlHistoryProvider | undefined {
|
||||
checkProposedApiEnabled(this._extension, 'scmHistoryProvider');
|
||||
return this._historyProvider;
|
||||
}
|
||||
|
||||
set historyProvider(historyProvider: vscode.SourceControlHistoryProvider | undefined) {
|
||||
checkProposedApiEnabled(this._extension, 'scmHistoryProvider');
|
||||
|
||||
this._historyProvider = historyProvider;
|
||||
this._historyProviderDisposable.value = new DisposableStore();
|
||||
|
||||
if (!historyProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#proxy.$registerHistoryProvider(this.handle);
|
||||
|
||||
this._historyProviderDisposable.value.add(historyProvider.onDidChangeCurrentHistoryItemGroup(() => {
|
||||
if (historyItemGroupEquals(this._historyProviderCurrentHistoryItemGroup, historyProvider?.currentHistoryItemGroup)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._historyProviderCurrentHistoryItemGroup = historyProvider?.currentHistoryItemGroup;
|
||||
this.#proxy.$onDidChangeHistoryProviderCurrentHistoryItemGroup(this.handle, this._historyProviderCurrentHistoryItemGroup);
|
||||
}));
|
||||
|
||||
this._historyProviderDisposable.value.add(historyProvider.onDidChangeActionButton(() => {
|
||||
checkProposedApiEnabled(this._extension, 'scmActionButton');
|
||||
|
||||
this._historyProviderActionButtonDisposable.value = new DisposableStore();
|
||||
const internal = historyProvider.actionButton !== undefined ?
|
||||
{
|
||||
command: this._commands.converter.toInternal(historyProvider.actionButton.command, this._historyProviderActionButtonDisposable.value),
|
||||
description: historyProvider.actionButton.description,
|
||||
enabled: historyProvider.actionButton.enabled
|
||||
} : undefined;
|
||||
|
||||
this.#proxy.$onDidChangeHistoryProviderActionButton(this.handle, internal ?? null);
|
||||
}));
|
||||
}
|
||||
|
||||
private _commitTemplate: string | undefined = undefined;
|
||||
|
||||
get commitTemplate(): string | undefined {
|
||||
|
@ -882,4 +953,28 @@ export class ExtHostSCM implements ExtHostSCMShape {
|
|||
this._selectedSourceControlHandle = selectedSourceControlHandle;
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
async $resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupId1: string, historyItemGroupId2: string | undefined, token: CancellationToken): Promise<{ id: string; ahead: number; behind: number } | undefined> {
|
||||
const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider;
|
||||
return await historyProvider?.resolveHistoryItemGroupCommonAncestor(historyItemGroupId1, historyItemGroupId2, token) ?? undefined;
|
||||
}
|
||||
|
||||
async $provideHistoryItems(sourceControlHandle: number, historyItemGroupId: string, options: any, token: CancellationToken): Promise<SCMHistoryItemDto[] | undefined> {
|
||||
const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider;
|
||||
const historyItems = await historyProvider?.provideHistoryItems(historyItemGroupId, options, token);
|
||||
|
||||
return historyItems?.map(item => ({
|
||||
id: item.id,
|
||||
parentIds: item.parentIds,
|
||||
label: item.label,
|
||||
description: item.description,
|
||||
icon: getHistoryItemIconDto(item),
|
||||
timestamp: item.timestamp,
|
||||
})) ?? undefined;
|
||||
}
|
||||
|
||||
async $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, token: CancellationToken): Promise<SCMHistoryItemChangeDto[] | undefined> {
|
||||
const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider;
|
||||
return await historyProvider?.provideHistoryItemChanges(historyItemId, token) ?? undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -110,12 +110,50 @@
|
|||
line-height: 22px;
|
||||
}
|
||||
|
||||
.scm-view .monaco-list-row .history,
|
||||
.scm-view .monaco-list-row .history-item-group,
|
||||
.scm-view .monaco-list-row .resource-group {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.scm-view .monaco-list-row .history-item-group .monaco-highlighted-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.scm-view .monaco-list-row .history-item-group .monaco-icon-label,
|
||||
.scm-view .monaco-list-row .history-item .monaco-icon-label {
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.scm-view .monaco-list-row .history-item-group .monaco-icon-label > .monaco-icon-label-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.scm-sync-view .monaco-list-row .monaco-icon-label .icon-container
|
||||
.scm-sync-view .monaco-list-row .monaco-icon-label .icon-container {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.scm-sync-view .monaco-list-row .history-item .monaco-icon-label .icon-container {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.scm-sync-view .monaco-list-row .history-item .monaco-icon-label .avatar {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.scm-view .monaco-list-row .history > .name,
|
||||
.scm-view .monaco-list-row .history-item-group > .name,
|
||||
.scm-view .monaco-list-row .resource-group > .name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -7,7 +7,7 @@ import { localize } from 'vs/nls';
|
|||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
|
||||
import { DirtyDiffWorkbenchController } from './dirtydiffDecorator';
|
||||
import { VIEWLET_ID, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm';
|
||||
import { VIEWLET_ID, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID, SYNC_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm';
|
||||
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { SCMActiveResourceContextKeyController, SCMStatusController } from './activity';
|
||||
|
@ -32,6 +32,7 @@ import { Context as SuggestContext } from 'vs/editor/contrib/suggest/browser/sug
|
|||
import { MANAGE_TRUST_COMMAND_ID, WorkspaceTrustContext } from 'vs/workbench/contrib/workspace/common/workspace';
|
||||
import { IQuickDiffService } from 'vs/workbench/contrib/scm/common/quickDiff';
|
||||
import { QuickDiffService } from 'vs/workbench/contrib/scm/common/quickDiffService';
|
||||
import { SCMSyncViewPane } from 'vs/workbench/contrib/scm/browser/scmSyncViewPane';
|
||||
|
||||
ModesRegistry.registerLanguage({
|
||||
id: 'scminput',
|
||||
|
@ -79,7 +80,7 @@ viewsRegistry.registerViews([{
|
|||
ctorDescriptor: new SyncDescriptor(SCMViewPane),
|
||||
canToggleVisibility: true,
|
||||
canMoveView: true,
|
||||
weight: 80,
|
||||
weight: 60,
|
||||
order: -999,
|
||||
containerIcon: sourceControlViewIcon,
|
||||
openCommandActionDescriptor: {
|
||||
|
@ -109,6 +110,17 @@ viewsRegistry.registerViews([{
|
|||
containerIcon: sourceControlViewIcon
|
||||
}], viewContainer);
|
||||
|
||||
viewsRegistry.registerViews([{
|
||||
id: SYNC_VIEW_PANE_ID,
|
||||
name: localize('source control sync', "Source Control Sync"),
|
||||
ctorDescriptor: new SyncDescriptor(SCMSyncViewPane),
|
||||
canToggleVisibility: false,
|
||||
canMoveView: true,
|
||||
weight: 20,
|
||||
order: -998,
|
||||
when: ContextKeyExpr.equals('config.scm.experimental.showSyncView', true),
|
||||
}], viewContainer);
|
||||
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench)
|
||||
.registerWorkbenchContribution(SCMActiveResourceContextKeyController, LifecyclePhase.Restored);
|
||||
|
||||
|
@ -279,6 +291,11 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis
|
|||
type: 'boolean',
|
||||
markdownDescription: localize('showActionButton', "Controls whether an action button can be shown in the Source Control view."),
|
||||
default: true
|
||||
},
|
||||
'scm.experimental.showSyncView': {
|
||||
type: 'boolean',
|
||||
description: localize('showSyncView', "Controls whether the Source Control Sync view is shown."),
|
||||
default: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
584
src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts
Normal file
584
src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts
Normal file
|
@ -0,0 +1,584 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as path from 'vs/base/common/path';
|
||||
import { append, $, prepend } from 'vs/base/browser/dom';
|
||||
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
|
||||
import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
|
||||
import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
|
||||
import { IAsyncDataSource, ITreeNode, ITreeRenderer, ITreeSorter } from 'vs/base/browser/ui/tree/tree';
|
||||
import { DisposableStore, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ThemeIcon } from 'vs/base/common/themables';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IOpenEvent, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
|
||||
import { API_OPEN_DIFF_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands';
|
||||
import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane';
|
||||
import { IViewDescriptorService } from 'vs/workbench/common/views';
|
||||
import { RepositoryRenderer } from 'vs/workbench/contrib/scm/browser/scmRepositoryRenderer';
|
||||
import { ActionButtonRenderer } from 'vs/workbench/contrib/scm/browser/scmViewPane';
|
||||
import { getActionViewItemProvider, isSCMActionButton, isSCMRepository, isSCMRepositoryArray } from 'vs/workbench/contrib/scm/browser/util';
|
||||
import { ISCMActionButton, ISCMRepository, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent } from 'vs/workbench/contrib/scm/common/scm';
|
||||
import { comparePaths } from 'vs/base/common/comparers';
|
||||
import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup } from 'vs/workbench/contrib/scm/common/history';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
|
||||
|
||||
type TreeElement = ISCMRepository[] | ISCMRepository | ISCMActionButton | SCMHistoryItemGroupTreeElement | SCMHistoryItemTreeElement | SCMHistoryItemChangeTreeElement;
|
||||
|
||||
function isSCMHistoryItemGroupTreeElement(obj: any): obj is SCMHistoryItemGroupTreeElement {
|
||||
return (obj as SCMHistoryItemGroupTreeElement).type === 'historyItemGroup';
|
||||
}
|
||||
|
||||
function isSCMHistoryItemTreeElement(obj: any): obj is SCMHistoryItemTreeElement {
|
||||
return (obj as SCMHistoryItemTreeElement).type === 'historyItem';
|
||||
}
|
||||
|
||||
function isSCMHistoryItemChangeTreeElement(obj: any): obj is SCMHistoryItemChangeTreeElement {
|
||||
return (obj as SCMHistoryItemChangeTreeElement).type === 'historyItemChange';
|
||||
}
|
||||
|
||||
function toDiffEditorArguments(uri: URI, originalUri: URI, modifiedUri: URI): unknown[] {
|
||||
const basename = path.basename(uri.fsPath);
|
||||
const originalQuery = JSON.parse(originalUri.query) as { path: string; ref: string };
|
||||
const modifiedQuery = JSON.parse(modifiedUri.query) as { path: string; ref: string };
|
||||
|
||||
const originalShortRef = originalQuery.ref.substring(0, 8).concat(originalQuery.ref.endsWith('^') ? '^' : '');
|
||||
const modifiedShortRef = modifiedQuery.ref.substring(0, 8).concat(modifiedQuery.ref.endsWith('^') ? '^' : '');
|
||||
|
||||
return [originalUri, modifiedUri, `${basename} (${originalShortRef}) ↔ ${basename} (${modifiedShortRef})`];
|
||||
}
|
||||
|
||||
function getSCMResourceId(element: TreeElement): string {
|
||||
if (isSCMRepository(element)) {
|
||||
const provider = element.provider;
|
||||
return `repo:${provider.id}`;
|
||||
} else if (isSCMActionButton(element)) {
|
||||
const provider = element.repository.provider;
|
||||
return `actionButton:${provider.id}`;
|
||||
} else if (isSCMHistoryItemGroupTreeElement(element)) {
|
||||
const provider = element.repository.provider;
|
||||
return `historyItemGroup:${provider.id}/${element.id}`;
|
||||
} else if (isSCMHistoryItemTreeElement(element)) {
|
||||
const historyItemGroup = element.historyItemGroup;
|
||||
const provider = historyItemGroup.repository.provider;
|
||||
return `historyItem:${provider.id}/${historyItemGroup.id}/${element.id}`;
|
||||
} else if (isSCMHistoryItemChangeTreeElement(element)) {
|
||||
const historyItem = element.historyItem;
|
||||
const historyItemGroup = historyItem.historyItemGroup;
|
||||
const provider = historyItemGroup.repository.provider;
|
||||
return `historyItemChange:${provider.id}/${historyItemGroup.id}/${historyItem.id}/${element.uri.toString()}`;
|
||||
} else {
|
||||
throw new Error('Invalid tree element');
|
||||
}
|
||||
}
|
||||
|
||||
interface SCMHistoryItemGroupTreeElement extends ISCMHistoryItemGroup {
|
||||
readonly description?: string;
|
||||
readonly ancestor?: string;
|
||||
readonly count?: number;
|
||||
readonly repository: ISCMRepository;
|
||||
readonly type: 'historyItemGroup';
|
||||
}
|
||||
|
||||
interface SCMHistoryItemTreeElement extends ISCMHistoryItem {
|
||||
readonly historyItemGroup: SCMHistoryItemGroupTreeElement;
|
||||
readonly type: 'historyItem';
|
||||
}
|
||||
|
||||
interface SCMHistoryItemChangeTreeElement extends ISCMHistoryItemChange {
|
||||
readonly historyItem: SCMHistoryItemTreeElement;
|
||||
readonly type: 'historyItemChange';
|
||||
}
|
||||
|
||||
class ListDelegate implements IListVirtualDelegate<any> {
|
||||
|
||||
getHeight(element: any): number {
|
||||
if (isSCMActionButton(element)) {
|
||||
return ActionButtonRenderer.DEFAULT_HEIGHT + 10;
|
||||
} else {
|
||||
return 22;
|
||||
}
|
||||
}
|
||||
|
||||
getTemplateId(element: any): string {
|
||||
if (isSCMRepository(element)) {
|
||||
return RepositoryRenderer.TEMPLATE_ID;
|
||||
} else if (isSCMActionButton(element)) {
|
||||
return ActionButtonRenderer.TEMPLATE_ID;
|
||||
} else if (isSCMHistoryItemGroupTreeElement(element)) {
|
||||
return HistoryItemGroupRenderer.TEMPLATE_ID;
|
||||
} else if (isSCMHistoryItemTreeElement(element)) {
|
||||
return HistoryItemRenderer.TEMPLATE_ID;
|
||||
} else if (isSCMHistoryItemChangeTreeElement(element)) {
|
||||
return HistoryItemChangeRenderer.TEMPLATE_ID;
|
||||
} else {
|
||||
throw new Error('Invalid tree element');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface HistoryItemGroupTemplate {
|
||||
readonly label: IconLabel;
|
||||
readonly count: CountBadge;
|
||||
readonly disposables: IDisposable;
|
||||
}
|
||||
|
||||
class HistoryItemGroupRenderer implements ITreeRenderer<SCMHistoryItemGroupTreeElement, void, HistoryItemGroupTemplate> {
|
||||
|
||||
static readonly TEMPLATE_ID = 'history-item-group';
|
||||
get templateId(): string { return HistoryItemGroupRenderer.TEMPLATE_ID; }
|
||||
|
||||
renderTemplate(container: HTMLElement) {
|
||||
// hack
|
||||
(container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-twistie');
|
||||
|
||||
const element = append(container, $('.history-item-group'));
|
||||
const label = new IconLabel(element, { supportIcons: true });
|
||||
const countContainer = append(element, $('.count'));
|
||||
const count = new CountBadge(countContainer, {}, defaultCountBadgeStyles);
|
||||
|
||||
return { label, count, disposables: new DisposableStore() };
|
||||
}
|
||||
|
||||
renderElement(node: ITreeNode<SCMHistoryItemGroupTreeElement>, index: number, templateData: HistoryItemGroupTemplate, height: number | undefined): void {
|
||||
const historyItemGroup = node.element;
|
||||
templateData.label.setLabel(historyItemGroup.label, historyItemGroup.description);
|
||||
templateData.count.setCount(historyItemGroup.count ?? 0);
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: HistoryItemGroupTemplate): void {
|
||||
templateData.disposables.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
interface HistoryItemTemplate {
|
||||
readonly iconContainer: HTMLElement;
|
||||
// readonly avatarImg: HTMLImageElement;
|
||||
readonly iconLabel: IconLabel;
|
||||
readonly timestampContainer: HTMLElement;
|
||||
readonly timestamp: HTMLSpanElement;
|
||||
readonly disposables: IDisposable;
|
||||
}
|
||||
|
||||
class HistoryItemRenderer implements ITreeRenderer<SCMHistoryItemTreeElement, void, HistoryItemTemplate> {
|
||||
|
||||
static readonly TEMPLATE_ID = 'history-item';
|
||||
get templateId(): string { return HistoryItemRenderer.TEMPLATE_ID; }
|
||||
|
||||
renderTemplate(container: HTMLElement): HistoryItemTemplate {
|
||||
// hack
|
||||
(container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-twistie');
|
||||
|
||||
const element = append(container, $('.history-item'));
|
||||
const iconLabel = new IconLabel(element, { supportIcons: true });
|
||||
|
||||
const iconContainer = prepend(iconLabel.element, $('.icon-container'));
|
||||
// const avatarImg = append(iconContainer, $('img.avatar')) as HTMLImageElement;
|
||||
|
||||
const timestampContainer = append(iconLabel.element, $('.timestamp-container'));
|
||||
const timestamp = append(timestampContainer, $('span.timestamp'));
|
||||
|
||||
return { iconContainer, iconLabel, timestampContainer, timestamp, disposables: new DisposableStore() };
|
||||
}
|
||||
|
||||
renderElement(node: ITreeNode<SCMHistoryItemTreeElement, void>, index: number, templateData: HistoryItemTemplate, height: number | undefined): void {
|
||||
const historyItem = node.element;
|
||||
|
||||
templateData.iconContainer.className = 'icon-container';
|
||||
if (historyItem.icon && ThemeIcon.isThemeIcon(historyItem.icon)) {
|
||||
templateData.iconContainer.classList.add(...ThemeIcon.asClassNameArray(historyItem.icon));
|
||||
}
|
||||
|
||||
// if (commit.authorAvatar) {
|
||||
// templateData.avatarImg.src = commit.authorAvatar;
|
||||
// templateData.avatarImg.style.display = 'block';
|
||||
// templateData.iconContainer.classList.remove(...ThemeIcon.asClassNameArray(Codicon.account));
|
||||
// } else {
|
||||
// templateData.avatarImg.style.display = 'none';
|
||||
// templateData.iconContainer.classList.add(...ThemeIcon.asClassNameArray(Codicon.account));
|
||||
// }
|
||||
|
||||
templateData.iconLabel.setLabel(historyItem.label, historyItem.description);
|
||||
|
||||
// templateData.timestampContainer.classList.toggle('timestamp-duplicate', commit.hideTimestamp === true);
|
||||
// templateData.timestamp.textContent = fromNow(commit.timestamp);
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: HistoryItemTemplate): void {
|
||||
templateData.disposables.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
interface HistoryItemChangeTemplate {
|
||||
readonly element: HTMLElement;
|
||||
readonly name: HTMLElement;
|
||||
readonly fileLabel: IResourceLabel;
|
||||
readonly decorationIcon: HTMLElement;
|
||||
readonly disposables: IDisposable;
|
||||
}
|
||||
|
||||
class HistoryItemChangeRenderer implements ITreeRenderer<SCMHistoryItemChangeTreeElement, void, HistoryItemChangeTemplate> {
|
||||
|
||||
static readonly TEMPLATE_ID = 'historyItemChange';
|
||||
get templateId(): string { return HistoryItemChangeRenderer.TEMPLATE_ID; }
|
||||
|
||||
constructor(private labels: ResourceLabels) { }
|
||||
|
||||
renderTemplate(container: HTMLElement): HistoryItemChangeTemplate {
|
||||
const element = append(container, $('.change'));
|
||||
const name = append(element, $('.name'));
|
||||
const fileLabel = this.labels.create(name, { supportDescriptionHighlights: true, supportHighlights: true });
|
||||
const decorationIcon = append(element, $('.decoration-icon'));
|
||||
|
||||
return { element, name, fileLabel, decorationIcon, disposables: new DisposableStore() };
|
||||
}
|
||||
|
||||
renderElement(node: ITreeNode<SCMHistoryItemChangeTreeElement, void>, index: number, templateData: HistoryItemChangeTemplate, height: number | undefined): void {
|
||||
templateData.fileLabel.setFile(node.element.uri, {
|
||||
fileDecorations: { colors: false, badges: true },
|
||||
hidePath: false,
|
||||
});
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: HistoryItemChangeTemplate): void {
|
||||
templateData.disposables.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class SCMSyncViewPaneAccessibilityProvider implements IListAccessibilityProvider<TreeElement> {
|
||||
|
||||
getAriaLabel(element: TreeElement): string {
|
||||
// TODO - add aria labels
|
||||
return '';
|
||||
}
|
||||
getWidgetAriaLabel(): string {
|
||||
return localize('scmSync', 'Source Control Sync');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SCMSyncViewPaneTreeIdentityProvider implements IIdentityProvider<TreeElement> {
|
||||
|
||||
getId(element: TreeElement): string {
|
||||
return getSCMResourceId(element);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SCMSyncViewPaneTreeSorter implements ITreeSorter<TreeElement> {
|
||||
|
||||
compare(element: TreeElement, otherElement: TreeElement): number {
|
||||
// Repository
|
||||
if (isSCMRepository(element)) {
|
||||
if (!isSCMRepository(otherElement)) {
|
||||
throw new Error('Invalid comparison');
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Action button
|
||||
if (isSCMActionButton(element)) {
|
||||
return -1;
|
||||
} else if (isSCMActionButton(otherElement)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// History item group
|
||||
if (isSCMHistoryItemGroupTreeElement(element)) {
|
||||
if (!isSCMHistoryItemGroupTreeElement(otherElement)) {
|
||||
throw new Error('Invalid comparison');
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// History item
|
||||
if (isSCMHistoryItemTreeElement(element)) {
|
||||
if (!isSCMHistoryItemTreeElement(otherElement)) {
|
||||
throw new Error('Invalid comparison');
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// History item change
|
||||
const elementPath = (element as SCMHistoryItemChangeTreeElement).uri.fsPath;
|
||||
const otherElementPath = (otherElement as SCMHistoryItemChangeTreeElement).uri.fsPath;
|
||||
|
||||
return comparePaths(elementPath, otherElementPath);
|
||||
}
|
||||
}
|
||||
|
||||
export class SCMSyncViewPane extends ViewPane {
|
||||
|
||||
private listLabels!: ResourceLabels;
|
||||
private _tree!: WorkbenchAsyncDataTree<TreeElement, TreeElement>;
|
||||
|
||||
private _viewModel!: SCMSyncPaneViewModel;
|
||||
get viewModel(): SCMSyncPaneViewModel { return this._viewModel; }
|
||||
|
||||
private readonly disposables = new DisposableStore();
|
||||
|
||||
constructor(
|
||||
options: IViewPaneOptions,
|
||||
@ICommandService private commandService: ICommandService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IOpenerService openerService: IOpenerService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@ITelemetryService telemetryService: ITelemetryService
|
||||
) {
|
||||
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
|
||||
}
|
||||
|
||||
protected override renderBody(container: HTMLElement): void {
|
||||
super.renderBody(container);
|
||||
|
||||
const treeContainer = append(container, $('.scm-view.scm-sync-view.file-icon-themable-tree'));
|
||||
|
||||
this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility });
|
||||
this._register(this.listLabels);
|
||||
|
||||
this._tree = this.instantiationService.createInstance(
|
||||
WorkbenchAsyncDataTree,
|
||||
'SCM Sync View',
|
||||
treeContainer,
|
||||
new ListDelegate(),
|
||||
[
|
||||
this.instantiationService.createInstance(RepositoryRenderer, getActionViewItemProvider(this.instantiationService)),
|
||||
this.instantiationService.createInstance(ActionButtonRenderer),
|
||||
this.instantiationService.createInstance(HistoryItemGroupRenderer),
|
||||
this.instantiationService.createInstance(HistoryItemRenderer),
|
||||
this.instantiationService.createInstance(HistoryItemChangeRenderer, this.listLabels),
|
||||
],
|
||||
this.instantiationService.createInstance(SCMSyncDataSource),
|
||||
{
|
||||
horizontalScrolling: false,
|
||||
accessibilityProvider: new SCMSyncViewPaneAccessibilityProvider(),
|
||||
identityProvider: new SCMSyncViewPaneTreeIdentityProvider(),
|
||||
sorter: new SCMSyncViewPaneTreeSorter(),
|
||||
}) as WorkbenchAsyncDataTree<TreeElement, TreeElement>;
|
||||
|
||||
this._register(this._tree);
|
||||
this._register(this._tree.onDidOpen(this.onDidOpen, this));
|
||||
|
||||
this._viewModel = this.instantiationService.createInstance(SCMSyncPaneViewModel, this._tree);
|
||||
}
|
||||
|
||||
private async onDidOpen(e: IOpenEvent<TreeElement | undefined>): Promise<void> {
|
||||
if (!e.element) {
|
||||
return;
|
||||
} else if (isSCMHistoryItemChangeTreeElement(e.element)) {
|
||||
if (e.element.originalUri && e.element.modifiedUri) {
|
||||
await this.commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, ...toDiffEditorArguments(e.element.uri, e.element.originalUri, e.element.modifiedUri));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this.disposables.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class SCMSyncPaneViewModel {
|
||||
|
||||
private repositories = new Map<ISCMRepository, IDisposable>();
|
||||
private alwaysShowRepositories = false;
|
||||
|
||||
private disposables = new DisposableStore();
|
||||
|
||||
constructor(
|
||||
private readonly tree: WorkbenchAsyncDataTree<TreeElement, TreeElement>,
|
||||
@ISCMViewService scmViewService: ISCMViewService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
|
||||
) {
|
||||
configurationService.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables);
|
||||
this.onDidChangeConfiguration();
|
||||
|
||||
scmViewService.onDidChangeVisibleRepositories(this._onDidChangeVisibleRepositories, this, this.disposables);
|
||||
this._onDidChangeVisibleRepositories({ added: scmViewService.visibleRepositories, removed: [] });
|
||||
}
|
||||
|
||||
private onDidChangeConfiguration(e?: IConfigurationChangeEvent): void {
|
||||
if (!e || e.affectsConfiguration('scm.alwaysShowRepositories')) {
|
||||
this.alwaysShowRepositories = this.configurationService.getValue<boolean>('scm.alwaysShowRepositories');
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private _onDidChangeVisibleRepositories({ added, removed }: ISCMViewVisibleRepositoryChangeEvent): void {
|
||||
for (const repository of added) {
|
||||
const repositoryDisposable: IDisposable = combinedDisposable(
|
||||
repository.provider.onDidChangeHistoryProviderActionButton(() => this.refresh(repository)),
|
||||
repository.provider.onDidChangeHistoryProviderCurrentHistoryItemGroup(() => this.refresh(repository))
|
||||
);
|
||||
|
||||
this.repositories.set(repository, { dispose() { repositoryDisposable.dispose(); } });
|
||||
}
|
||||
|
||||
for (const repository of removed) {
|
||||
this.repositories.get(repository)?.dispose();
|
||||
this.repositories.delete(repository);
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private async refresh(repository?: ISCMRepository): Promise<void> {
|
||||
if (this.repositories.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (repository) {
|
||||
// Particular repository
|
||||
await this.tree.updateChildren(repository);
|
||||
} else if (this.repositories.size === 1 && !this.alwaysShowRepositories) {
|
||||
// Single repository and not always show repositories
|
||||
await this.tree.setInput(Iterable.first(this.repositories.keys())!);
|
||||
} else {
|
||||
// Expand repository nodes
|
||||
const expanded = Array.from(this.repositories.keys())
|
||||
.map(repository => `repo:${repository.provider.id}`);
|
||||
|
||||
// Multiple repositories or always show repositories
|
||||
await this.tree.setInput([...this.repositories.keys()], { expanded });
|
||||
}
|
||||
this.tree.layout();
|
||||
}
|
||||
}
|
||||
|
||||
class SCMSyncDataSource implements IAsyncDataSource<TreeElement, TreeElement> {
|
||||
|
||||
hasChildren(element: TreeElement): boolean {
|
||||
if (isSCMRepositoryArray(element)) {
|
||||
return true;
|
||||
} else if (isSCMRepository(element)) {
|
||||
return true;
|
||||
} else if (isSCMActionButton(element)) {
|
||||
return false;
|
||||
} else if (isSCMHistoryItemGroupTreeElement(element)) {
|
||||
return true;
|
||||
} else if (isSCMHistoryItemTreeElement(element)) {
|
||||
return true;
|
||||
} else if (isSCMHistoryItemChangeTreeElement(element)) {
|
||||
return false;
|
||||
} else {
|
||||
throw new Error('hasChildren not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
async getChildren(element: TreeElement): Promise<TreeElement[]> {
|
||||
const children: TreeElement[] = [];
|
||||
|
||||
if (isSCMRepositoryArray(element)) {
|
||||
children.push(...element);
|
||||
} else if (isSCMRepository(element)) {
|
||||
const scmProvider = element.provider;
|
||||
const historyProvider = scmProvider.historyProvider;
|
||||
const historyItemGroup = historyProvider?.currentHistoryItemGroup();
|
||||
|
||||
if (!historyProvider || !historyItemGroup) {
|
||||
return children;
|
||||
}
|
||||
|
||||
// Action Button
|
||||
const actionButton = historyProvider.actionButton();
|
||||
if (actionButton) {
|
||||
children.push({
|
||||
type: 'actionButton',
|
||||
repository: element,
|
||||
button: actionButton
|
||||
} as ISCMActionButton);
|
||||
}
|
||||
|
||||
// Common ancestor, ahead, behind
|
||||
const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(historyItemGroup.id, historyItemGroup.upstream?.id);
|
||||
|
||||
// Incoming
|
||||
if (historyItemGroup?.upstream) {
|
||||
children.push({
|
||||
id: historyItemGroup.upstream.id,
|
||||
label: localize('incoming', "$(cloud-download) Incoming Changes"),
|
||||
description: historyItemGroup.upstream.label,
|
||||
ancestor: ancestor?.id,
|
||||
count: ancestor?.behind ?? 0,
|
||||
repository: element,
|
||||
type: 'historyItemGroup'
|
||||
} as SCMHistoryItemGroupTreeElement);
|
||||
}
|
||||
|
||||
// Outgoing
|
||||
if (historyItemGroup) {
|
||||
children.push({
|
||||
id: historyItemGroup.id,
|
||||
label: localize('outgoing', "$(cloud-upload) Outgoing Changes"),
|
||||
description: historyItemGroup.label,
|
||||
ancestor: ancestor?.id,
|
||||
count: ancestor?.ahead ?? 0,
|
||||
repository: element,
|
||||
type: 'historyItemGroup'
|
||||
} as SCMHistoryItemGroupTreeElement);
|
||||
}
|
||||
} else if (isSCMHistoryItemGroupTreeElement(element)) {
|
||||
const scmProvider = element.repository.provider;
|
||||
const historyProvider = scmProvider.historyProvider;
|
||||
|
||||
if (!historyProvider) {
|
||||
return children;
|
||||
}
|
||||
|
||||
const historyItems = await historyProvider.provideHistoryItems(element.id, { limit: { id: element.ancestor } }) ?? [];
|
||||
children.push(...historyItems.map(historyItem => ({
|
||||
id: historyItem.id,
|
||||
label: historyItem.label,
|
||||
description: historyItem.description,
|
||||
icon: historyItem.icon,
|
||||
historyItemGroup: element,
|
||||
type: 'historyItem'
|
||||
} as SCMHistoryItemTreeElement)));
|
||||
} else if (isSCMHistoryItemTreeElement(element)) {
|
||||
const repository = element.historyItemGroup.repository;
|
||||
const historyProvider = repository.provider.historyProvider;
|
||||
|
||||
if (!historyProvider) {
|
||||
return children;
|
||||
}
|
||||
|
||||
// History Item Changes
|
||||
const changes = await historyProvider.provideHistoryItemChanges(element.id) ?? [];
|
||||
children.push(...changes.map(change => ({
|
||||
uri: change.uri,
|
||||
originalUri: change.originalUri,
|
||||
modifiedUri: change.modifiedUri,
|
||||
renameUri: change.renameUri,
|
||||
historyItem: element,
|
||||
type: 'historyItemChange'
|
||||
} as SCMHistoryItemChangeTreeElement)));
|
||||
} else {
|
||||
throw new Error('getChildren Method not implemented.');
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
}
|
|
@ -112,7 +112,7 @@ interface ActionButtonTemplate {
|
|||
readonly templateDisposable: IDisposable;
|
||||
}
|
||||
|
||||
class ActionButtonRenderer implements ICompressibleTreeRenderer<ISCMActionButton, FuzzyScore, ActionButtonTemplate> {
|
||||
export class ActionButtonRenderer implements ICompressibleTreeRenderer<ISCMActionButton, FuzzyScore, ActionButtonTemplate> {
|
||||
static readonly DEFAULT_HEIGHT = 30;
|
||||
|
||||
static readonly TEMPLATE_ID = 'actionButton';
|
||||
|
|
|
@ -17,6 +17,10 @@ import { Command } from 'vs/editor/common/languages';
|
|||
import { reset } from 'vs/base/browser/dom';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export function isSCMRepositoryArray(element: any): element is ISCMRepository[] {
|
||||
return Array.isArray(element) && element.every(r => isSCMRepository(r));
|
||||
}
|
||||
|
||||
export function isSCMRepository(element: any): element is ISCMRepository {
|
||||
return !!(element as ISCMRepository).provider && !!(element as ISCMRepository).input;
|
||||
}
|
||||
|
|
43
src/vs/workbench/contrib/scm/common/history.ts
Normal file
43
src/vs/workbench/contrib/scm/common/history.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ThemeIcon } from 'vs/base/common/themables';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ISCMActionButtonDescriptor } from 'vs/workbench/contrib/scm/common/scm';
|
||||
|
||||
export interface ISCMHistoryProvider {
|
||||
actionButton: () => ISCMActionButtonDescriptor | undefined;
|
||||
currentHistoryItemGroup: () => ISCMHistoryItemGroup | undefined;
|
||||
provideHistoryItems(historyItemGroupId: string, options: ISCMHistoryOptions): Promise<ISCMHistoryItem[] | undefined>;
|
||||
provideHistoryItemChanges(historyItemId: string): Promise<ISCMHistoryItemChange[] | undefined>;
|
||||
resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined>;
|
||||
}
|
||||
|
||||
export interface ISCMHistoryOptions {
|
||||
readonly cursor?: string;
|
||||
readonly limit?: number | { id?: string };
|
||||
}
|
||||
|
||||
export interface ISCMHistoryItemGroup {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly upstream?: ISCMHistoryItemGroup;
|
||||
}
|
||||
|
||||
export interface ISCMHistoryItem {
|
||||
readonly id: string;
|
||||
readonly parentIds: string[];
|
||||
readonly label: string;
|
||||
readonly description?: string;
|
||||
readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon;
|
||||
readonly timestamp?: number;
|
||||
}
|
||||
|
||||
export interface ISCMHistoryItemChange {
|
||||
readonly uri: URI;
|
||||
readonly originalUri?: URI;
|
||||
readonly modifiedUri?: URI;
|
||||
readonly renameUri?: URI;
|
||||
}
|
|
@ -13,10 +13,12 @@ import { IAction } from 'vs/base/common/actions';
|
|||
import { IMenu } from 'vs/platform/actions/common/actions';
|
||||
import { ThemeIcon } from 'vs/base/common/themables';
|
||||
import { IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { ISCMHistoryProvider } from 'vs/workbench/contrib/scm/common/history';
|
||||
|
||||
export const VIEWLET_ID = 'workbench.view.scm';
|
||||
export const VIEW_PANE_ID = 'workbench.scm';
|
||||
export const REPOSITORIES_VIEW_PANE_ID = 'workbench.scm.repositories';
|
||||
export const SYNC_VIEW_PANE_ID = 'workbench.scm.sync';
|
||||
|
||||
export interface IBaselineResourceProvider {
|
||||
getBaselineResource(resource: URI): Promise<URI>;
|
||||
|
@ -63,7 +65,10 @@ export interface ISCMProvider extends IDisposable {
|
|||
readonly inputBoxDocumentUri: URI;
|
||||
readonly count?: number;
|
||||
readonly commitTemplate: string;
|
||||
readonly historyProvider?: ISCMHistoryProvider;
|
||||
readonly onDidChangeCommitTemplate: Event<string>;
|
||||
readonly onDidChangeHistoryProviderActionButton: Event<void>;
|
||||
readonly onDidChangeHistoryProviderCurrentHistoryItemGroup: Event<void>;
|
||||
readonly onDidChangeStatusBarCommands?: Event<readonly Command[]>;
|
||||
readonly acceptInputCommand?: Command;
|
||||
readonly actionButton?: ISCMActionButtonDescriptor;
|
||||
|
|
|
@ -73,6 +73,7 @@ export const allApiProposals = Object.freeze({
|
|||
resolvers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.resolvers.d.ts',
|
||||
saveEditor: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.saveEditor.d.ts',
|
||||
scmActionButton: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmActionButton.d.ts',
|
||||
scmHistoryProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts',
|
||||
scmSelectedProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts',
|
||||
scmTextDocument: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmTextDocument.d.ts',
|
||||
scmValidation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmValidation.d.ts',
|
||||
|
|
72
src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts
vendored
Normal file
72
src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts
vendored
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
declare module 'vscode' {
|
||||
// https://github.com/microsoft/vscode/issues/185269
|
||||
|
||||
export interface SourceControl {
|
||||
historyProvider?: SourceControlHistoryProvider;
|
||||
}
|
||||
|
||||
export interface SourceControlHistoryProvider {
|
||||
actionButton?: SourceControlActionButton;
|
||||
currentHistoryItemGroup?: SourceControlHistoryItemGroup;
|
||||
|
||||
/**
|
||||
* Fires when the action button changes
|
||||
*/
|
||||
onDidChangeActionButton: Event<void>;
|
||||
|
||||
/**
|
||||
* Fires when the current history item group changes (ex: checkout)
|
||||
*/
|
||||
onDidChangeCurrentHistoryItemGroup: Event<void>;
|
||||
|
||||
/**
|
||||
* Fires when the history item groups change (ex: commit, push, fetch)
|
||||
*/
|
||||
// onDidChangeHistoryItemGroups: Event<SourceControlHistoryChangeEvent>;
|
||||
|
||||
provideHistoryItems(historyItemGroupId: string, options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult<SourceControlHistoryItem[]>;
|
||||
provideHistoryItemChanges(historyItemId: string, token: CancellationToken): ProviderResult<SourceControlHistoryItemChange[]>;
|
||||
resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined, token: CancellationToken): ProviderResult<{ id: string; ahead: number; behind: number }>;
|
||||
|
||||
// resolveHistoryItemGroup(historyItemGroupId: string, token: CancellationToken): ProviderResult<SourceControlHistoryItemGroup | undefined>;
|
||||
}
|
||||
|
||||
export interface SourceControlHistoryOptions {
|
||||
readonly cursor?: string;
|
||||
readonly limit?: number | { id?: string };
|
||||
}
|
||||
|
||||
export interface SourceControlHistoryItemGroup {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly upstream?: SourceControlHistoryItemGroup;
|
||||
}
|
||||
|
||||
export interface SourceControlHistoryItem {
|
||||
readonly id: string;
|
||||
readonly parentIds: string[];
|
||||
readonly label: string;
|
||||
readonly description?: string;
|
||||
readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon;
|
||||
readonly timestamp?: number;
|
||||
}
|
||||
|
||||
export interface SourceControlHistoryItemChange {
|
||||
readonly uri: Uri;
|
||||
readonly originalUri: Uri | undefined;
|
||||
readonly modifiedUri: Uri | undefined;
|
||||
readonly renameUri: Uri | undefined;
|
||||
}
|
||||
|
||||
// export interface SourceControlHistoryChangeEvent {
|
||||
// readonly added: Iterable<SourceControlHistoryItemGroup>;
|
||||
// readonly removed: Iterable<SourceControlHistoryItemGroup>;
|
||||
// readonly modified: Iterable<SourceControlHistoryItemGroup>;
|
||||
// }
|
||||
|
||||
}
|
Loading…
Reference in a new issue