mirror of
https://github.com/Microsoft/vscode
synced 2024-10-02 17:32:41 +00:00
SCM - Add proposal for inputBox action button (#196121)
* SCM - Add proposal for inputBox action button * More reliable fix for overlapping
This commit is contained in:
parent
0e925d6700
commit
c05b49710b
|
@ -23,7 +23,8 @@
|
|||
"scmValidation",
|
||||
"tabInputTextMerge",
|
||||
"timeline",
|
||||
"contribMergeEditorMenus"
|
||||
"contribMergeEditorMenus",
|
||||
"scmInputBoxActionButton"
|
||||
],
|
||||
"categories": [
|
||||
"Other"
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { Model } from '../model';
|
||||
import { Repository as BaseRepository, Resource } from '../repository';
|
||||
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions } from './git';
|
||||
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions, CommitMessageProvider } from './git';
|
||||
import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands, CancellationToken } from 'vscode';
|
||||
import { combinedDisposable, mapEvent } from '../util';
|
||||
import { toGitUri } from '../uri';
|
||||
|
@ -341,6 +341,10 @@ export class ApiImpl implements API {
|
|||
return this._model.registerBranchProtectionProvider(root, provider);
|
||||
}
|
||||
|
||||
registerCommitMessageProvider(provider: CommitMessageProvider): Disposable {
|
||||
return this._model.registerCommitMessageProvider(provider);
|
||||
}
|
||||
|
||||
constructor(private _model: Model) { }
|
||||
}
|
||||
|
||||
|
|
9
extensions/git/src/api/git.d.ts
vendored
9
extensions/git/src/api/git.d.ts
vendored
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Uri, Event, Disposable, ProviderResult, Command, CancellationToken } from 'vscode';
|
||||
import { Uri, Event, Disposable, ProviderResult, Command, CancellationToken, ThemeIcon } from 'vscode';
|
||||
export { ProviderResult } from 'vscode';
|
||||
|
||||
export interface Git {
|
||||
|
@ -300,6 +300,12 @@ export interface BranchProtectionProvider {
|
|||
provideBranchProtection(): BranchProtection[];
|
||||
}
|
||||
|
||||
export interface CommitMessageProvider {
|
||||
readonly title: string;
|
||||
readonly icon?: Uri | { light: Uri, dark: Uri } | ThemeIcon;
|
||||
provideCommitMessage(changes: string[], cancellationToken?: CancellationToken): Promise<string | undefined>;
|
||||
}
|
||||
|
||||
export type APIState = 'uninitialized' | 'initialized';
|
||||
|
||||
export interface PublishEvent {
|
||||
|
@ -327,6 +333,7 @@ export interface API {
|
|||
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable;
|
||||
registerPushErrorHandler(handler: PushErrorHandler): Disposable;
|
||||
registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable;
|
||||
registerCommitMessageProvider(provider: CommitMessageProvider): Disposable;
|
||||
}
|
||||
|
||||
export interface GitExtension {
|
||||
|
|
|
@ -3581,6 +3581,26 @@ export class CommandCenter {
|
|||
}
|
||||
}
|
||||
|
||||
@command('git.generateCommitMessage', { repository: true })
|
||||
async generateCommitMessage(repository: Repository): Promise<void> {
|
||||
if (!repository || !this.model.commitMessageProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
await window.withProgress({ location: ProgressLocation.SourceControl }, async () => {
|
||||
await repository.generateCommitMessage();
|
||||
});
|
||||
}
|
||||
|
||||
@command('git.generateCommitMessageCancel', { repository: true })
|
||||
generateCommitMessageCancel(repository: Repository): void {
|
||||
if (!repository || !this.model.commitMessageProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
repository.generateCommitMessageCancel();
|
||||
}
|
||||
|
||||
private createCommand(id: string, key: string, method: Function, options: ScmCommandOptions): (...args: any[]) => any {
|
||||
const result = (...args: any[]) => {
|
||||
let result: Promise<any>;
|
||||
|
|
154
extensions/git/src/commitMessageProvider.ts
Normal file
154
extensions/git/src/commitMessageProvider.ts
Normal file
|
@ -0,0 +1,154 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken, Disposable, Event, EventEmitter, Uri, workspace, SourceControlInputBoxActionButton, ThemeIcon, l10n } from 'vscode';
|
||||
import { CommitMessageProvider, Status } from './api/git';
|
||||
import { Repository } from './repository';
|
||||
import { dispose } from './util';
|
||||
|
||||
export interface ICommitMessageProviderRegistry {
|
||||
readonly onDidChangeCommitMessageProvider: Event<void>;
|
||||
|
||||
commitMessageProvider: CommitMessageProvider | undefined;
|
||||
registerCommitMessageProvider(provider: CommitMessageProvider): Disposable;
|
||||
}
|
||||
|
||||
export class TestCommitMessageProvider implements CommitMessageProvider {
|
||||
|
||||
readonly icon = new ThemeIcon('rocket');
|
||||
readonly title = 'Generate Commit Message (Test)';
|
||||
|
||||
async provideCommitMessage(_: string[], token: CancellationToken): Promise<string | undefined> {
|
||||
if (token.isCancellationRequested) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
token.onCancellationRequested(() => resolve(undefined));
|
||||
setTimeout(() => resolve(`Test commit message (${Math.random()})`), 5000);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface ActionButtonState {
|
||||
readonly isGenerating: boolean;
|
||||
readonly enabled: boolean;
|
||||
}
|
||||
|
||||
export class GenerateCommitMessageActionButton {
|
||||
|
||||
private _onDidChange = new EventEmitter<void>();
|
||||
get onDidChange(): Event<void> { return this._onDidChange.event; }
|
||||
|
||||
private _state: ActionButtonState;
|
||||
get state() { return this._state; }
|
||||
set state(state: ActionButtonState) {
|
||||
if (this._state.enabled === state.enabled &&
|
||||
this._state.isGenerating === state.isGenerating) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._state = state;
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
|
||||
get button(): SourceControlInputBoxActionButton | undefined {
|
||||
if (this.commitMessageProviderRegistry.commitMessageProvider === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.state.isGenerating ?
|
||||
{
|
||||
icon: new ThemeIcon('debug-stop'),
|
||||
command: { title: l10n.t('Cancel'), command: 'git.generateCommitMessageCancel' },
|
||||
enabled: this.state.enabled
|
||||
} :
|
||||
{
|
||||
icon: this.commitMessageProviderRegistry.commitMessageProvider.icon ?? new ThemeIcon('sparkle'),
|
||||
command: { title: this.commitMessageProviderRegistry.commitMessageProvider.title, command: 'git.generateCommitMessage' },
|
||||
enabled: this.state.enabled
|
||||
};
|
||||
}
|
||||
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly repository: Repository,
|
||||
private readonly commitMessageProviderRegistry: ICommitMessageProviderRegistry
|
||||
) {
|
||||
this._state = {
|
||||
enabled: false,
|
||||
isGenerating: false
|
||||
};
|
||||
|
||||
const root = Uri.file(repository.root);
|
||||
this.disposables.push(workspace.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('git.enableSmartCommit', root) ||
|
||||
e.affectsConfiguration('git.smartCommitChanges', root) ||
|
||||
e.affectsConfiguration('git.suggestSmartCommit', root)) {
|
||||
this.onDidChangeSmartCommitSettings();
|
||||
}
|
||||
}));
|
||||
repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables);
|
||||
repository.onDidStartCommitMessageGeneration(this.onDidStartCommitMessageGeneration, this, this.disposables);
|
||||
repository.onDidEndCommitMessageGeneration(this.onDidEndCommitMessageGeneration, this, this.disposables);
|
||||
commitMessageProviderRegistry.onDidChangeCommitMessageProvider(this.onDidChangeCommitMessageProvider, this, this.disposables);
|
||||
}
|
||||
|
||||
private onDidChangeCommitMessageProvider(): void {
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
|
||||
private onDidStartCommitMessageGeneration(): void {
|
||||
this.state = { ...this.state, isGenerating: true };
|
||||
}
|
||||
|
||||
private onDidEndCommitMessageGeneration(): void {
|
||||
this.state = { ...this.state, isGenerating: false };
|
||||
}
|
||||
|
||||
private onDidChangeSmartCommitSettings(): void {
|
||||
this.state = {
|
||||
...this.state,
|
||||
enabled: this.repositoryHasChangesToCommit()
|
||||
};
|
||||
}
|
||||
|
||||
private onDidRunGitStatus(): void {
|
||||
this.state = {
|
||||
...this.state,
|
||||
enabled: this.repositoryHasChangesToCommit()
|
||||
};
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -12,13 +12,14 @@ import { Git } from './git';
|
|||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { fromGitUri } from './uri';
|
||||
import { APIState as State, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher, PostCommitCommandsProvider, BranchProtectionProvider } from './api/git';
|
||||
import { APIState as State, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher, PostCommitCommandsProvider, BranchProtectionProvider, CommitMessageProvider } from './api/git';
|
||||
import { Askpass } from './askpass';
|
||||
import { IPushErrorHandlerRegistry } from './pushError';
|
||||
import { ApiRepository } from './api/api1';
|
||||
import { IRemoteSourcePublisherRegistry } from './remotePublisher';
|
||||
import { IPostCommitCommandsProviderRegistry } from './postCommitCommands';
|
||||
import { IBranchProtectionProviderRegistry } from './branchProtection';
|
||||
import { ICommitMessageProviderRegistry } from './commitMessageProvider';
|
||||
|
||||
class RepositoryPick implements QuickPickItem {
|
||||
@memoize get label(): string {
|
||||
|
@ -170,7 +171,7 @@ class UnsafeRepositoriesManager {
|
|||
}
|
||||
}
|
||||
|
||||
export class Model implements IRepositoryResolver, IBranchProtectionProviderRegistry, IRemoteSourcePublisherRegistry, IPostCommitCommandsProviderRegistry, IPushErrorHandlerRegistry {
|
||||
export class Model implements IRepositoryResolver, IBranchProtectionProviderRegistry, ICommitMessageProviderRegistry, IRemoteSourcePublisherRegistry, IPostCommitCommandsProviderRegistry, IPushErrorHandlerRegistry {
|
||||
|
||||
private _onDidOpenRepository = new EventEmitter<Repository>();
|
||||
readonly onDidOpenRepository: Event<Repository> = this._onDidOpenRepository.event;
|
||||
|
@ -237,6 +238,14 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi
|
|||
|
||||
private pushErrorHandlers = new Set<PushErrorHandler>();
|
||||
|
||||
private _commitMessageProvider: CommitMessageProvider | undefined;
|
||||
get commitMessageProvider(): CommitMessageProvider | undefined {
|
||||
return this._commitMessageProvider;
|
||||
}
|
||||
|
||||
private _onDidChangeCommitMessageProvider = new EventEmitter<void>();
|
||||
readonly onDidChangeCommitMessageProvider = this._onDidChangeCommitMessageProvider.event;
|
||||
|
||||
private _unsafeRepositoriesManager: UnsafeRepositoriesManager;
|
||||
get unsafeRepositories(): string[] {
|
||||
return this._unsafeRepositoriesManager.repositories;
|
||||
|
@ -578,7 +587,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi
|
|||
|
||||
// Open repository
|
||||
const [dotGit, repositoryRootRealPath] = await Promise.all([this.git.getRepositoryDotGit(repositoryRoot), this.getRepositoryRootRealPath(repositoryRoot)]);
|
||||
const repository = new Repository(this.git.open(repositoryRoot, repositoryRootRealPath, dotGit, this.logger), this, this, this, this, this, this.globalState, this.logger, this.telemetryReporter);
|
||||
const repository = new Repository(this.git.open(repositoryRoot, repositoryRootRealPath, dotGit, this.logger), this, this, this, this, this, this, this.globalState, this.logger, this.telemetryReporter);
|
||||
|
||||
this.open(repository);
|
||||
this._closedRepositoriesManager.deleteRepository(repository.root);
|
||||
|
@ -939,6 +948,16 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi
|
|||
return toDisposable(() => this.pushErrorHandlers.delete(handler));
|
||||
}
|
||||
|
||||
registerCommitMessageProvider(provider: CommitMessageProvider): Disposable {
|
||||
this._commitMessageProvider = provider;
|
||||
this._onDidChangeCommitMessageProvider.fire();
|
||||
|
||||
return toDisposable(() => {
|
||||
this._commitMessageProvider = undefined;
|
||||
this._onDidChangeCommitMessageProvider.fire();
|
||||
});
|
||||
}
|
||||
|
||||
getPushErrorHandlers(): PushErrorHandler[] {
|
||||
return [...this.pushErrorHandlers];
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import { IPostCommitCommandsProviderRegistry, CommitCommandsCenter } from './pos
|
|||
import { Operation, OperationKind, OperationManager, OperationResult } from './operation';
|
||||
import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection';
|
||||
import { GitHistoryProvider } from './historyProvider';
|
||||
import { GenerateCommitMessageActionButton, ICommitMessageProviderRegistry } from './commitMessageProvider';
|
||||
|
||||
const timeout = (millis: number) => new Promise(c => setTimeout(c, millis));
|
||||
|
||||
|
@ -673,6 +674,12 @@ export class Repository implements Disposable {
|
|||
private _onDidChangeBranchProtection = new EventEmitter<void>();
|
||||
readonly onDidChangeBranchProtection: Event<void> = this._onDidChangeBranchProtection.event;
|
||||
|
||||
private _onDidStartCommitMessageGeneration = new EventEmitter<void>();
|
||||
readonly onDidStartCommitMessageGeneration: Event<void> = this._onDidStartCommitMessageGeneration.event;
|
||||
|
||||
private _onDidEndCommitMessageGeneration = new EventEmitter<void>();
|
||||
readonly onDidEndCommitMessageGeneration: Event<void> = this._onDidEndCommitMessageGeneration.event;
|
||||
|
||||
@memoize
|
||||
get onDidChangeOperations(): Event<void> {
|
||||
return anyEvent(this.onRunOperation as Event<any>, this.onDidRunOperation as Event<any>);
|
||||
|
@ -797,6 +804,7 @@ export class Repository implements Disposable {
|
|||
private commitCommandCenter: CommitCommandsCenter;
|
||||
private resourceCommandResolver = new ResourceCommandResolver(this);
|
||||
private updateModelStateCancellationTokenSource: CancellationTokenSource | undefined;
|
||||
private generateCommitMessageCancellationTokenSource: CancellationTokenSource | undefined;
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(
|
||||
|
@ -806,6 +814,7 @@ export class Repository implements Disposable {
|
|||
remoteSourcePublisherRegistry: IRemoteSourcePublisherRegistry,
|
||||
postCommitCommandsProviderRegistry: IPostCommitCommandsProviderRegistry,
|
||||
private readonly branchProtectionProviderRegistry: IBranchProtectionProviderRegistry,
|
||||
private readonly commitMessageProviderRegistry: ICommitMessageProviderRegistry,
|
||||
globalState: Memento,
|
||||
private readonly logger: LogOutputChannel,
|
||||
private telemetryReporter: TelemetryReporter
|
||||
|
@ -850,6 +859,12 @@ export class Repository implements Disposable {
|
|||
|
||||
this._sourceControl.acceptInputCommand = { command: 'git.commit', title: l10n.t('Commit'), arguments: [this._sourceControl] };
|
||||
this._sourceControl.inputBox.validateInput = this.validateInput.bind(this);
|
||||
|
||||
const inputActionButton = new GenerateCommitMessageActionButton(this, commitMessageProviderRegistry);
|
||||
this.disposables.push(inputActionButton);
|
||||
inputActionButton.onDidChange(() => this._sourceControl.inputBox.actionButton = inputActionButton.button);
|
||||
this._sourceControl.inputBox.actionButton = inputActionButton.button;
|
||||
|
||||
this.disposables.push(this._sourceControl);
|
||||
|
||||
this.updateInputBoxPlaceholder();
|
||||
|
@ -2006,6 +2021,54 @@ export class Repository implements Disposable {
|
|||
});
|
||||
}
|
||||
|
||||
async generateCommitMessage(): Promise<void> {
|
||||
if (!this.commitMessageProviderRegistry.commitMessageProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._onDidStartCommitMessageGeneration.fire();
|
||||
this.generateCommitMessageCancellationTokenSource?.cancel();
|
||||
this.generateCommitMessageCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
try {
|
||||
const diff: string[] = [];
|
||||
if (this.indexGroup.resourceStates.length !== 0) {
|
||||
for (const file of this.indexGroup.resourceStates.map(r => r.resourceUri.fsPath)) {
|
||||
diff.push(await this.diffIndexWithHEAD(file));
|
||||
}
|
||||
} else {
|
||||
for (const file of this.workingTreeGroup.resourceStates.map(r => r.resourceUri.fsPath)) {
|
||||
diff.push(await this.diffWithHEAD(file));
|
||||
}
|
||||
}
|
||||
|
||||
if (diff.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = this.generateCommitMessageCancellationTokenSource.token;
|
||||
const provider = this.commitMessageProviderRegistry.commitMessageProvider;
|
||||
const commitMessage = await provider.provideCommitMessage(diff, token);
|
||||
if (commitMessage) {
|
||||
this.inputBox.value = commitMessage;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
this.logger.error(err);
|
||||
}
|
||||
finally {
|
||||
this._onDidEndCommitMessageGeneration.fire();
|
||||
}
|
||||
}
|
||||
|
||||
generateCommitMessageCancel(): void {
|
||||
this.generateCommitMessageCancellationTokenSource?.cancel();
|
||||
this.generateCommitMessageCancellationTokenSource?.dispose();
|
||||
this.generateCommitMessageCancellationTokenSource = undefined;
|
||||
|
||||
this._onDidEndCommitMessageGeneration.fire();
|
||||
}
|
||||
|
||||
// Parses output of `git check-ignore -v -z` and returns only those paths
|
||||
// that are actually ignored by git.
|
||||
// Matches to a negative pattern (starting with '!') are filtered out.
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"../../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.scmInputBoxActionButton.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, SCMHistoryItemDto, SCMActionButtonDto, SCMHistoryItemGroupDto } from '../common/extHost.protocol';
|
||||
import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemDto, SCMActionButtonDto, SCMHistoryItemGroupDto, SCMInputActionButtonDto } 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';
|
||||
|
@ -18,6 +18,19 @@ import { IMarkdownString } from 'vs/base/common/htmlContent';
|
|||
import { IQuickDiffService, QuickDiffProvider } from 'vs/workbench/contrib/scm/common/quickDiff';
|
||||
import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup, ISCMHistoryOptions, ISCMHistoryProvider } from 'vs/workbench/contrib/scm/common/history';
|
||||
|
||||
function getSCMInputBoxActionButtonIcon(actionButton: SCMInputActionButtonDto): URI | { light: URI; dark: URI } | ThemeIcon | undefined {
|
||||
if (!actionButton.icon) {
|
||||
return undefined;
|
||||
} else if (URI.isUri(actionButton.icon)) {
|
||||
return URI.revive(actionButton.icon);
|
||||
} else if (ThemeIcon.isThemeIcon(actionButton.icon)) {
|
||||
return actionButton.icon;
|
||||
} else {
|
||||
const icon = actionButton.icon as { light: UriComponents; dark: UriComponents };
|
||||
return { light: URI.revive(icon.light), dark: URI.revive(icon.dark) };
|
||||
}
|
||||
}
|
||||
|
||||
function getSCMHistoryItemIcon(historyItem: SCMHistoryItemDto): URI | { light: URI; dark: URI } | ThemeIcon | undefined {
|
||||
if (!historyItem.icon) {
|
||||
return undefined;
|
||||
|
@ -554,6 +567,16 @@ export class MainThreadSCM implements MainThreadSCMShape {
|
|||
repository.input.visible = visible;
|
||||
}
|
||||
|
||||
$setInputBoxActionButton(sourceControlHandle: number, actionButton?: SCMInputActionButtonDto | null | undefined): void {
|
||||
const repository = this._repositories.get(sourceControlHandle);
|
||||
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
repository.input.actionButton = actionButton ? { ...actionButton, icon: getSCMInputBoxActionButtonIcon(actionButton) } : undefined;
|
||||
}
|
||||
|
||||
$showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType) {
|
||||
const repository = this._repositories.get(sourceControlHandle);
|
||||
if (!repository) {
|
||||
|
|
|
@ -1415,6 +1415,12 @@ export interface SCMActionButtonDto {
|
|||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface SCMInputActionButtonDto {
|
||||
command: ICommandDto;
|
||||
icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface SCMGroupFeatures {
|
||||
hideWhenEmpty?: boolean;
|
||||
}
|
||||
|
@ -1484,6 +1490,7 @@ export interface MainThreadSCMShape extends IDisposable {
|
|||
$setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): void;
|
||||
$setInputBoxEnablement(sourceControlHandle: number, enabled: boolean): void;
|
||||
$setInputBoxVisibility(sourceControlHandle: number, visible: boolean): void;
|
||||
$setInputBoxActionButton(sourceControlHandle: number, actionButton?: SCMInputActionButtonDto | null): void;
|
||||
$showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType): void;
|
||||
$setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): void;
|
||||
|
||||
|
|
|
@ -45,6 +45,19 @@ function getIconResource(decorations?: vscode.SourceControlResourceThemableDecor
|
|||
}
|
||||
}
|
||||
|
||||
function getInputBoxActionButtonIcon(actionButton?: vscode.SourceControlInputBoxActionButton): UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon | undefined {
|
||||
if (!actionButton?.icon) {
|
||||
return undefined;
|
||||
} else if (URI.isUri(actionButton.icon)) {
|
||||
return actionButton.icon;
|
||||
} else if (ThemeIcon.isThemeIcon(actionButton.icon)) {
|
||||
return actionButton.icon;
|
||||
} else {
|
||||
const icon = actionButton.icon as { light: URI; dark: URI };
|
||||
return { light: icon.light, dark: icon.dark };
|
||||
}
|
||||
}
|
||||
|
||||
function getHistoryItemIconDto(historyItem: vscode.SourceControlHistoryItem): UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon | undefined {
|
||||
if (!historyItem.icon) {
|
||||
return undefined;
|
||||
|
@ -313,13 +326,36 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox {
|
|||
this.#proxy.$setInputBoxVisibility(this._sourceControlHandle, visible);
|
||||
}
|
||||
|
||||
private _actionButton: vscode.SourceControlInputBoxActionButton | undefined;
|
||||
private _actionButtonDisposables = new MutableDisposable<DisposableStore>();
|
||||
|
||||
get actionButton(): vscode.SourceControlInputBoxActionButton | undefined {
|
||||
checkProposedApiEnabled(this._extension, 'scmInputBoxActionButton');
|
||||
return this._actionButton;
|
||||
}
|
||||
|
||||
set actionButton(actionButton: vscode.SourceControlInputBoxActionButton | undefined) {
|
||||
checkProposedApiEnabled(this._extension, 'scmInputBoxActionButton');
|
||||
this._actionButtonDisposables.value = new DisposableStore();
|
||||
|
||||
this._actionButton = actionButton;
|
||||
|
||||
const internal = actionButton !== undefined ?
|
||||
{
|
||||
command: this._commands.converter.toInternal(actionButton.command, this._actionButtonDisposables.value),
|
||||
icon: getInputBoxActionButtonIcon(actionButton),
|
||||
enabled: actionButton.enabled
|
||||
} : undefined;
|
||||
this.#proxy.$setInputBoxActionButton(this._sourceControlHandle, internal ?? null);
|
||||
}
|
||||
|
||||
get document(): vscode.TextDocument {
|
||||
checkProposedApiEnabled(this._extension, 'scmTextDocument');
|
||||
|
||||
return this.#extHostDocuments.getDocument(this._documentUri);
|
||||
}
|
||||
|
||||
constructor(private _extension: IExtensionDescription, _extHostDocuments: ExtHostDocuments, proxy: MainThreadSCMShape, private _sourceControlHandle: number, private _documentUri: URI) {
|
||||
constructor(private _extension: IExtensionDescription, private _commands: ExtHostCommands, _extHostDocuments: ExtHostDocuments, proxy: MainThreadSCMShape, private _sourceControlHandle: number, private _documentUri: URI) {
|
||||
this.#extHostDocuments = _extHostDocuments;
|
||||
this.#proxy = proxy;
|
||||
}
|
||||
|
@ -680,7 +716,7 @@ class ExtHostSourceControl implements vscode.SourceControl {
|
|||
query: _rootUri ? `rootUri=${encodeURIComponent(_rootUri.toString())}` : undefined
|
||||
});
|
||||
|
||||
this._inputBox = new ExtHostSCMInputBox(_extension, _extHostDocuments, this.#proxy, this.handle, inputBoxDocumentUri);
|
||||
this._inputBox = new ExtHostSCMInputBox(_extension, _commands, _extHostDocuments, this.#proxy, this.handle, inputBoxDocumentUri);
|
||||
this.#proxy.$registerSourceControl(this.handle, _id, _label, _rootUri, inputBoxDocumentUri);
|
||||
}
|
||||
|
||||
|
|
|
@ -199,16 +199,12 @@
|
|||
flex-grow: 100;
|
||||
}
|
||||
|
||||
.scm-view .monaco-list .monaco-list-row .scm-input > .actions,
|
||||
.scm-view .monaco-list .monaco-list-row .resource-group > .actions,
|
||||
.scm-view .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions {
|
||||
display: none;
|
||||
max-width: fit-content;
|
||||
}
|
||||
|
||||
.scm-view .monaco-list .monaco-list-row:hover .scm-input > .actions,
|
||||
.scm-view .monaco-list .monaco-list-row.selected .scm-input > .actions,
|
||||
.scm-view .monaco-list .monaco-list-row.focused .scm-input > .actions,
|
||||
.scm-view .monaco-list .monaco-list-row:hover .resource-group > .actions,
|
||||
.scm-view .monaco-list .monaco-list-row.selected .resource-group > .actions,
|
||||
.scm-view .monaco-list .monaco-list-row.focused .resource-group > .actions,
|
||||
|
@ -231,7 +227,7 @@
|
|||
}
|
||||
|
||||
.scm-view.show-actions .scm-provider > .actions,
|
||||
.scm-view.show-actions > .monaco-list .monaco-list-row .scm-input > .actions,
|
||||
.scm-view.show-actions > .monaco-list .monaco-list-row .scm-input > .scm-editor > .actions,
|
||||
.scm-view.show-actions > .monaco-list .monaco-list-row .resource-group > .actions,
|
||||
.scm-view.show-actions > .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions {
|
||||
display: block;
|
||||
|
@ -251,6 +247,11 @@
|
|||
position: absolute;
|
||||
top: 7px;
|
||||
right: 20px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.scm-view .scm-input .actions .actions-container {
|
||||
background-color: var(--vscode-input-background);
|
||||
}
|
||||
|
||||
.scm-view .scm-input .actions .action-label {
|
||||
|
@ -418,7 +419,6 @@
|
|||
z-index: 1;
|
||||
padding: 2px 6px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
|
|
@ -227,7 +227,6 @@ class SCMTreeDragAndDrop implements ITreeDragAndDrop<TreeElement> {
|
|||
interface InputTemplate {
|
||||
readonly inputWidget: SCMInputWidget;
|
||||
inputWidgetHeight: number;
|
||||
actionBar: ActionBar;
|
||||
readonly elementDisposables: DisposableStore;
|
||||
readonly templateDisposable: IDisposable;
|
||||
}
|
||||
|
@ -247,9 +246,7 @@ class InputRenderer implements ICompressibleTreeRenderer<ISCMInput, FuzzyScore,
|
|||
private outerLayout: ISCMLayout,
|
||||
private overflowWidgetsDomNode: HTMLElement,
|
||||
private updateHeight: (input: ISCMInput, height: number) => void,
|
||||
private actionViewItemProvider: IActionViewItemProvider,
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
@ISCMViewService private scmViewService: ISCMViewService
|
||||
@IInstantiationService private instantiationService: IInstantiationService
|
||||
) { }
|
||||
|
||||
renderTemplate(container: HTMLElement): InputTemplate {
|
||||
|
@ -264,10 +261,7 @@ class InputRenderer implements ICompressibleTreeRenderer<ISCMInput, FuzzyScore,
|
|||
const inputWidget = this.instantiationService.createInstance(SCMInputWidget, inputElement, this.overflowWidgetsDomNode);
|
||||
templateDisposable.add(inputWidget);
|
||||
|
||||
const actionsContainer = append(inputElement, $('.actions'));
|
||||
const actionBar = new ActionBar(actionsContainer, { actionViewItemProvider: this.actionViewItemProvider });
|
||||
|
||||
return { inputWidget, inputWidgetHeight: InputRenderer.DEFAULT_HEIGHT, actionBar, elementDisposables: new DisposableStore(), templateDisposable };
|
||||
return { inputWidget, inputWidgetHeight: InputRenderer.DEFAULT_HEIGHT, elementDisposables: new DisposableStore(), templateDisposable };
|
||||
}
|
||||
|
||||
renderElement(node: ITreeNode<ISCMInput, FuzzyScore>, index: number, templateData: InputTemplate): void {
|
||||
|
@ -320,13 +314,6 @@ class InputRenderer implements ICompressibleTreeRenderer<ISCMInput, FuzzyScore,
|
|||
const layoutEditor = () => templateData.inputWidget.layout();
|
||||
templateData.elementDisposables.add(this.outerLayout.onDidChange(layoutEditor));
|
||||
layoutEditor();
|
||||
|
||||
// Action bar
|
||||
templateData.actionBar.clear();
|
||||
templateData.actionBar.context = input.repository.provider;
|
||||
|
||||
const menus = this.scmViewService.menus.getRepositoryMenus(input.repository.provider);
|
||||
templateData.elementDisposables.add(connectPrimaryMenuToInlineActionBar(menus.inputBoxMenu, templateData.actionBar));
|
||||
}
|
||||
|
||||
renderCompressedElements(): void {
|
||||
|
@ -1285,6 +1272,7 @@ class ViewModel {
|
|||
for (const repository of added) {
|
||||
const disposable = combinedDisposable(
|
||||
repository.provider.groups.onDidSplice(splice => this._onDidSpliceGroups(item, splice)),
|
||||
repository.input.onDidChangeActionButton(() => this.refresh(item)),
|
||||
repository.input.onDidChangeVisibility(() => this.refresh(item)),
|
||||
repository.provider.onDidChange(() => {
|
||||
if (this.showActionButton) {
|
||||
|
@ -1820,6 +1808,7 @@ class SCMInputWidget {
|
|||
private editorContainer: HTMLElement;
|
||||
private placeholderTextContainer: HTMLElement;
|
||||
private inputEditor: CodeEditorWidget;
|
||||
private actionBar: ActionBar;
|
||||
private readonly disposables = new DisposableStore();
|
||||
|
||||
private model: { readonly input: ISCMInput; textModelRef?: IReference<IResolvedTextEditorModel> } | undefined;
|
||||
|
@ -1968,6 +1957,31 @@ class SCMInputWidget {
|
|||
};
|
||||
this.repositoryDisposables.add(input.onDidChangeEnablement(enabled => updateEnablement(enabled)));
|
||||
updateEnablement(input.enabled);
|
||||
|
||||
// ActionBar
|
||||
this.actionBar.context = input.repository.provider;
|
||||
|
||||
const onDidChangeActionButton = () => {
|
||||
this.actionBar.clear();
|
||||
if (!input.actionButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
const action = new Action(
|
||||
input.actionButton.command.id,
|
||||
input.actionButton.command.title,
|
||||
ThemeIcon.isThemeIcon(input.actionButton.icon) ? ThemeIcon.asClassName(input.actionButton.icon) : undefined,
|
||||
input.actionButton.enabled,
|
||||
() => this.commandService.executeCommand(input.actionButton!.command.id, ...(input.actionButton!.command.arguments || [])));
|
||||
|
||||
this.actionBar.push(action, { icon: true, label: false });
|
||||
|
||||
// Update placeholder width to accommodate for the action bar
|
||||
this.placeholderTextContainer.style.width = input.actionButton ? 'calc(100% - 26px)' : '100%';
|
||||
};
|
||||
|
||||
this.repositoryDisposables.add(input.onDidChangeActionButton(onDidChangeActionButton, this));
|
||||
onDidChangeActionButton();
|
||||
}
|
||||
|
||||
get selections(): Selection[] | null {
|
||||
|
@ -2010,6 +2024,7 @@ class SCMInputWidget {
|
|||
@ISCMViewService private readonly scmViewService: ISCMViewService,
|
||||
@IContextViewService private readonly contextViewService: IContextViewService,
|
||||
@IOpenerService private readonly openerService: IOpenerService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
) {
|
||||
this.element = append(container, $('.scm-editor'));
|
||||
this.editorContainer = append(this.element, $('.scm-editor-container'));
|
||||
|
@ -2063,6 +2078,9 @@ class SCMInputWidget {
|
|||
])
|
||||
};
|
||||
|
||||
this.actionBar = new ActionBar(append(this.element, $('.actions')));
|
||||
this.disposables.add(this.actionBar);
|
||||
|
||||
const services = new ServiceCollection([IContextKeyService, contextKeyService2]);
|
||||
const instantiationService2 = instantiationService.createChild(services);
|
||||
this.inputEditor = instantiationService2.createInstance(CodeEditorWidget, this.editorContainer, editorOptions, codeEditorWidgetOptions);
|
||||
|
@ -2361,7 +2379,7 @@ export class SCMViewPane extends ViewPane {
|
|||
this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.providerCountBadge'), this.disposables)(updateProviderCountVisibility));
|
||||
updateProviderCountVisibility();
|
||||
|
||||
this.inputRenderer = this.instantiationService.createInstance(InputRenderer, this.layoutCache, overflowWidgetsDomNode, (input, height) => this.tree.updateElementHeight(input, height), getActionViewItemProvider(this.instantiationService));
|
||||
this.inputRenderer = this.instantiationService.createInstance(InputRenderer, this.layoutCache, overflowWidgetsDomNode, (input, height) => this.tree.updateElementHeight(input, height));
|
||||
const delegate = new ListDelegate(this.inputRenderer);
|
||||
|
||||
this.actionButtonRenderer = this.instantiationService.createInstance(ActionButtonRenderer);
|
||||
|
|
|
@ -102,6 +102,12 @@ export interface ISCMInputChangeEvent {
|
|||
readonly reason?: SCMInputChangeReason;
|
||||
}
|
||||
|
||||
export interface ISCMInputActionButtonDescriptor {
|
||||
command: Command;
|
||||
icon?: URI | { light: URI; dark: URI } | ThemeIcon;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface ISCMActionButtonDescriptor {
|
||||
command: Command;
|
||||
secondaryCommands?: Command[][];
|
||||
|
@ -134,6 +140,9 @@ export interface ISCMInput {
|
|||
visible: boolean;
|
||||
readonly onDidChangeVisibility: Event<boolean>;
|
||||
|
||||
actionButton: ISCMInputActionButtonDescriptor | undefined;
|
||||
readonly onDidChangeActionButton: Event<void>;
|
||||
|
||||
setFocus(): void;
|
||||
readonly onDidChangeFocus: Event<void>;
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { ISCMService, ISCMProvider, ISCMInput, ISCMRepository, IInputValidator, ISCMInputChangeEvent, SCMInputChangeReason, InputValidationType, IInputValidation } from './scm';
|
||||
import { ISCMService, ISCMProvider, ISCMInput, ISCMRepository, IInputValidator, ISCMInputChangeEvent, SCMInputChangeReason, InputValidationType, IInputValidation, ISCMActionButtonDescriptor } from './scm';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
|
@ -69,6 +69,19 @@ class SCMInput implements ISCMInput {
|
|||
private readonly _onDidChangeVisibility = new Emitter<boolean>();
|
||||
readonly onDidChangeVisibility: Event<boolean> = this._onDidChangeVisibility.event;
|
||||
|
||||
private _actionButton: ISCMActionButtonDescriptor | undefined;
|
||||
get actionButton(): ISCMActionButtonDescriptor | undefined {
|
||||
return this._actionButton;
|
||||
}
|
||||
|
||||
set actionButton(actionButton: ISCMActionButtonDescriptor) {
|
||||
this._actionButton = actionButton;
|
||||
this._onDidChangeActionButton.fire();
|
||||
}
|
||||
|
||||
private readonly _onDidChangeActionButton = new Emitter<void>();
|
||||
readonly onDidChangeActionButton: Event<void> = this._onDidChangeActionButton.event;
|
||||
|
||||
setFocus(): void {
|
||||
this._onDidChangeFocus.fire();
|
||||
}
|
||||
|
|
|
@ -80,6 +80,7 @@ export const allApiProposals = Object.freeze({
|
|||
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',
|
||||
scmInputBoxActionButton: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmInputBoxActionButton.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',
|
||||
|
|
19
src/vscode-dts/vscode.proposed.scmInputBoxActionButton.d.ts
vendored
Normal file
19
src/vscode-dts/vscode.proposed.scmInputBoxActionButton.d.ts
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/195474
|
||||
|
||||
export interface SourceControlInputBoxActionButton {
|
||||
readonly command: Command;
|
||||
readonly enabled: boolean;
|
||||
readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon;
|
||||
}
|
||||
|
||||
export interface SourceControlInputBox {
|
||||
actionButton?: SourceControlInputBoxActionButton;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue