Files
vscode/src/vs/sessions/contrib/changes/browser/changesViewActions.ts
T

504 lines
17 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Codicon } from '../../../../base/common/codicons.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { observableFromEvent } from '../../../../base/common/observable.js';
import { isEqual } from '../../../../base/common/resources.js';
import { URI } from '../../../../base/common/uri.js';
import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';
import { localize, localize2 } from '../../../../nls.js';
import { Action2, IAction2Options, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { DIFF_VIEW_MODE_INLINE_TEMPORARY, SET_DIFF_VIEW_MODE_AUTOMATIC, SET_DIFF_VIEW_MODE_INLINE, SET_DIFF_VIEW_MODE_SIDE_BY_SIDE, TOGGLE_DIFF_SIDE_BY_SIDE } from '../../../../workbench/browser/parts/editor/diffEditorCommands.js';
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';
import { ActiveEditorContext, AuxiliaryBarVisibleContext, IsAuxiliaryWindowContext, IsSessionsWindowContext, IsTopRightEditorGroupContext, MainEditorAreaVisibleContext, TextCompareEditorActiveContext } from '../../../../workbench/common/contextkeys.js';
import { DiffEditorInput } from '../../../../workbench/common/editor/diffEditorInput.js';
import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js';
import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js';
import { MultiDiffEditor } from '../../../../workbench/contrib/multiDiffEditor/browser/multiDiffEditor.js';
import { Menus } from '../../../browser/menus.js';
import { CustomViewVisibleContext, SessionHasChangesContext, SessionIsCreatedContext, SinglePaneDiffEditorInputActiveContext, SinglePaneLayoutEnabledContext } from '../../../common/contextkeys.js';
import { logChangesViewViewModeChange } from '../../../common/sessionsTelemetry.js';
import { ISessionsService } from '../../../services/sessions/browser/sessionsService.js';
import { OPEN_PULL_REQUEST_ACTION_ID } from '../../github/common/types.js';
import { ActiveSessionContextKeys, CHANGES_VIEW_ID, ChangesContextKeys, ChangesViewMode, SESSIONS_CHANGES_OPEN_SINGLE_FILE_DIFF_SETTING } from '../common/changes.js';
import { IChangesViewService } from '../common/changesViewService.js';
import { SessionsDiffViewModeContext } from '../../editor/common/diffEditorOptionsService.js';
import { CHANGES_HEADER_ACTIONS_ID } from './changesView.js';
import { SessionChangesEditor } from './sessionChangesEditor.js';
const openChangesViewActionOptions: IAction2Options = {
id: 'workbench.action.agentSessions.openChangesView',
title: localize2('openChangesView', "Changes"),
icon: Codicon.diffMultiple,
f1: false,
};
class OpenChangesViewAction extends Action2 {
static readonly ID = openChangesViewActionOptions.id;
constructor() {
super(openChangesViewActionOptions);
}
async run(accessor: ServicesAccessor): Promise<void> {
const viewsService = accessor.get(IViewsService);
await viewsService.openView(CHANGES_VIEW_ID, true);
}
}
registerAction2(OpenChangesViewAction);
class ChangesViewActionsContribution extends Disposable implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.changesViewActions';
constructor(
@IContextKeyService contextKeyService: IContextKeyService,
@ISessionsService sessionsService: ISessionsService,
@IChangesViewService changesViewService: IChangesViewService,
@IEditorService editorService: IEditorService,
) {
super();
// Bind context key: true when the active session has changes
this._register(bindContextKey(ActiveSessionContextKeys.HasChanges, contextKeyService, reader => {
const activeSession = sessionsService.activeSession.read(reader);
if (!activeSession) {
return false;
}
const changes = activeSession.changes.read(reader);
return changes.length > 0;
}));
this._register(bindContextKey(ChangesContextKeys.ViewMode, contextKeyService, reader => {
return changesViewService.viewModeObs.read(reader);
}));
const activeEditor = observableFromEvent(this, editorService.onDidActiveEditorChange, () => editorService.activeEditor);
this._register(bindContextKey(SinglePaneDiffEditorInputActiveContext, contextKeyService, reader => activeEditor.read(reader) instanceof DiffEditorInput));
}
}
registerWorkbenchContribution2(ChangesViewActionsContribution.ID, ChangesViewActionsContribution, WorkbenchPhase.AfterRestored);
class OpenPullRequestAction extends Action2 {
static readonly ID = 'workbench.action.agentSessions.openPullRequest';
constructor() {
super({
id: OpenPullRequestAction.ID,
title: localize2('openPullRequest', "Open Pull Request"),
icon: Codicon.gitPullRequest,
f1: false,
menu: {
id: MenuId.AgentsChangesToolbar,
group: 'navigation',
order: 9,
when: ContextKeyExpr.and(
IsSessionsWindowContext,
ActiveSessionContextKeys.HasPullRequest)
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandService = accessor.get(ICommandService);
const sessionsService = accessor.get(ISessionsService);
const activeSession = sessionsService.activeSession.get();
if (!activeSession) {
return;
}
await commandService.executeCommand(OPEN_PULL_REQUEST_ACTION_ID, activeSession);
}
}
registerAction2(OpenPullRequestAction);
const agentsChangesEditorActive = ContextKeyExpr.and(
IsSessionsWindowContext,
ActiveEditorContext.isEqualTo(SessionChangesEditor.ID)
);
const singlePaneFileDiffEditorActive = ContextKeyExpr.and(
IsSessionsWindowContext,
SinglePaneDiffEditorInputActiveContext,
SinglePaneLayoutEnabledContext
);
const agentsTextDiffEditorActive = ContextKeyExpr.and(
IsSessionsWindowContext,
TextCompareEditorActiveContext
);
const agentsMultiDiffEditorActive = ContextKeyExpr.and(
IsSessionsWindowContext,
ActiveEditorContext.isEqualTo(MultiDiffEditor.ID)
);
const agentsDiffEditorActive = ContextKeyExpr.or(
agentsChangesEditorActive,
agentsTextDiffEditorActive,
agentsMultiDiffEditorActive
);
const singlePaneChangesEditorActive = ContextKeyExpr.and(agentsChangesEditorActive, SinglePaneLayoutEnabledContext);
const singlePaneTextDiffEditorActive = ContextKeyExpr.and(agentsTextDiffEditorActive, SinglePaneLayoutEnabledContext);
// Title-bar (tab-row) gate that does NOT require the editor content area to be
// visible, so session-level title actions (e.g. Create Pull Request) stay available
// when the editor area is closed but the docked tab bar is still shown.
const singlePaneChangesEditorTitle = ContextKeyExpr.and(
singlePaneChangesEditorActive,
IsAuxiliaryWindowContext.toNegated(),
IsTopRightEditorGroupContext
);
const singlePaneChangesEditorTitleVisible = ContextKeyExpr.and(
singlePaneChangesEditorTitle,
MainEditorAreaVisibleContext
);
const singlePaneDiffEditorTitle = ContextKeyExpr.and(
ContextKeyExpr.or(singlePaneChangesEditorActive, singlePaneFileDiffEditorActive),
IsAuxiliaryWindowContext.toNegated(),
IsTopRightEditorGroupContext
);
const singlePaneTextDiffEditorTitle = ContextKeyExpr.and(
singlePaneTextDiffEditorActive,
IsAuxiliaryWindowContext.toNegated(),
IsTopRightEditorGroupContext
);
const singlePaneMultiDiffEditorTitle = ContextKeyExpr.and(
agentsMultiDiffEditorActive,
SinglePaneLayoutEnabledContext,
IsAuxiliaryWindowContext.toNegated(),
IsTopRightEditorGroupContext
);
const singlePaneDiffEditorTitleVisible = ContextKeyExpr.and(
ContextKeyExpr.or(
singlePaneChangesEditorTitle,
singlePaneTextDiffEditorTitle,
singlePaneMultiDiffEditorTitle
),
MainEditorAreaVisibleContext
);
/** Anchor action hosting the Create Pull Request button bar in the title bar. */
class ChangesHeaderActionsAction extends Action2 {
constructor() {
super({
id: CHANGES_HEADER_ACTIONS_ID,
title: localize2('changesView.headerActions', "Changes Actions"),
f1: false,
menu: {
id: Menus.TitleBarSessionMenu,
group: 'navigation',
order: 5,
when: ContextKeyExpr.and(
IsSessionsWindowContext,
IsAuxiliaryWindowContext.toNegated(),
CustomViewVisibleContext.negate(),
SinglePaneLayoutEnabledContext,
SessionIsCreatedContext,
SessionHasChangesContext
)
},
});
}
override async run(): Promise<void> { }
}
registerAction2(ChangesHeaderActionsAction);
class SetChangesListViewModeAction extends Action2 {
static readonly ID = 'workbench.action.agentSessions.setChangesListViewMode';
constructor() {
super({
id: SetChangesListViewModeAction.ID,
title: localize2('agentSessions.setChangesListViewMode', "View as List"),
icon: Codicon.listFlat,
f1: false,
menu: {
id: Menus.SessionsEditorHeaderLayout,
group: 'secondary/2_viewMode',
order: 20,
when: ContextKeyExpr.and(
singlePaneDiffEditorTitle,
AuxiliaryBarVisibleContext,
ChangesContextKeys.ViewMode.isEqualTo(ChangesViewMode.Tree))
}
});
}
run(accessor: ServicesAccessor): void {
logChangesViewViewModeChange(accessor.get(ITelemetryService), ChangesViewMode.List);
accessor.get(IChangesViewService).setViewMode(ChangesViewMode.List);
}
}
registerAction2(SetChangesListViewModeAction);
class SetChangesTreeViewModeAction extends Action2 {
static readonly ID = 'workbench.action.agentSessions.setChangesTreeViewMode';
constructor() {
super({
id: SetChangesTreeViewModeAction.ID,
title: localize2('agentSessions.setChangesTreeViewMode', "View as Tree"),
icon: Codicon.listTree,
f1: false,
menu: {
id: Menus.SessionsEditorHeaderLayout,
group: 'secondary/2_viewMode',
order: 20,
when: ContextKeyExpr.and(
singlePaneDiffEditorTitle,
AuxiliaryBarVisibleContext,
ChangesContextKeys.ViewMode.isEqualTo(ChangesViewMode.List))
}
});
}
run(accessor: ServicesAccessor): void {
logChangesViewViewModeChange(accessor.get(ITelemetryService), ChangesViewMode.Tree);
accessor.get(IChangesViewService).setViewMode(ChangesViewMode.Tree);
}
}
registerAction2(SetChangesTreeViewModeAction);
class CollapseAllSessionChangesDiffsAction extends Action2 {
static readonly ID = 'workbench.action.agentSessions.collapseAllDiffs';
constructor() {
super({
id: CollapseAllSessionChangesDiffsAction.ID,
title: localize2('agentSessions.collapseAllDiffs', "Collapse All Diffs"),
icon: Codicon.collapseAll,
f1: false,
menu: {
id: Menus.SessionsEditorHeaderLayout,
group: 'secondary/1_diff',
order: 10,
when: ContextKeyExpr.and(
singlePaneChangesEditorTitleVisible,
ContextKeyExpr.not('multiDiffEditorAllCollapsed'))
}
});
}
run(accessor: ServicesAccessor): void {
const activeEditorPane = accessor.get(IEditorService).activeEditorPane;
if (activeEditorPane instanceof SessionChangesEditor) {
activeEditorPane.collapseAllDiffs();
}
}
}
registerAction2(CollapseAllSessionChangesDiffsAction);
class ExpandAllSessionChangesDiffsAction extends Action2 {
static readonly ID = 'workbench.action.agentSessions.expandAllDiffs';
constructor() {
super({
id: ExpandAllSessionChangesDiffsAction.ID,
title: localize2('agentSessions.expandAllDiffs', "Expand All Diffs"),
icon: Codicon.expandAll,
f1: false,
menu: {
id: Menus.SessionsEditorHeaderLayout,
group: 'secondary/1_diff',
order: 10,
when: ContextKeyExpr.and(
singlePaneChangesEditorActive,
IsAuxiliaryWindowContext.toNegated(),
IsTopRightEditorGroupContext,
MainEditorAreaVisibleContext,
ContextKeyExpr.has('multiDiffEditorAllCollapsed'))
}
});
}
run(accessor: ServicesAccessor): void {
const activeEditorPane = accessor.get(IEditorService).activeEditorPane;
if (activeEditorPane instanceof SessionChangesEditor) {
activeEditorPane.expandAllDiffs();
}
}
}
registerAction2(ExpandAllSessionChangesDiffsAction);
// The Agents window reuses the workbench `toggle.diff.renderSideBySide` command so a
// user's keybinding for it carries over here (issue #324765). The sessions override of
// IDiffEditorCommandsService updates the Changes editor's own preferred layout.
MenuRegistry.appendMenuItem(Menus.SessionsEditorTitle, {
submenu: Menus.SessionsDiffEditorView,
title: localize('diffView', "Diff View"),
group: '1_diff',
order: 10,
when: singlePaneDiffEditorTitleVisible,
});
MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
submenu: Menus.SessionsDiffEditorView,
title: localize('diffView', "Diff View"),
group: '1_diff',
order: 10,
when: ContextKeyExpr.and(agentsDiffEditorActive, SinglePaneLayoutEnabledContext.negate()),
});
MenuRegistry.appendMenuItem(Menus.SessionsDiffEditorView, {
command: {
id: SET_DIFF_VIEW_MODE_INLINE,
title: localize('diffView.inline', "Inline"),
toggled: SessionsDiffViewModeContext.isEqualTo('inline'),
},
group: '1_view',
order: 1,
});
MenuRegistry.appendMenuItem(Menus.SessionsDiffEditorView, {
command: {
id: SET_DIFF_VIEW_MODE_SIDE_BY_SIDE,
title: localize('diffView.sideBySide', "Side by Side"),
toggled: SessionsDiffViewModeContext.isEqualTo('sideBySide'),
},
group: '1_view',
order: 2,
});
for (const [title, when] of [
[localize('diffView.automaticSideBySide', "Automatic (Currently Side by Side)"), EditorContextKeys.diffEditorAutomaticRenderSideBySide],
[localize('diffView.automaticInline', "Automatic (Currently Inline)"), EditorContextKeys.diffEditorAutomaticRenderSideBySide.toNegated()],
] as const) {
MenuRegistry.appendMenuItem(Menus.SessionsDiffEditorView, {
command: {
id: SET_DIFF_VIEW_MODE_AUTOMATIC,
title,
toggled: ContextKeyExpr.and(
SessionsDiffViewModeContext.isEqualTo('automatic'),
EditorContextKeys.diffEditorTemporaryInlineMode.toNegated(),
),
},
group: '1_view',
order: 3,
when,
});
}
MenuRegistry.appendMenuItem(Menus.SessionsDiffEditorView, {
command: {
id: DIFF_VIEW_MODE_INLINE_TEMPORARY,
title: localize('diffView.inlineTemporary', "Inline (Temporary)"),
toggled: EditorContextKeys.diffEditorTemporaryInlineMode,
precondition: ContextKeyExpr.false(),
},
group: '1_view',
order: 4,
when: EditorContextKeys.diffEditorTemporaryInlineMode,
});
// Discoverable in the command palette while a Changes diff editor is visible.
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id: TOGGLE_DIFF_SIDE_BY_SIDE,
title: localize2('togglePreferredDiffView', "Toggle Preferred Diff View"),
category: localize2('changes', "Changes"),
},
when: agentsDiffEditorActive
});
class OpenChangesAction extends Action2 {
static readonly ID = 'workbench.action.agentSessions.openChanges';
constructor() {
super({
id: OpenChangesAction.ID,
title: localize2('openChanges', "Open Changes"),
icon: Codicon.gitCompare,
f1: false
});
}
async run(accessor: ServicesAccessor, _sessionResource: URI, _ref: string, ...resources: URI[]): Promise<void> {
const editorService = accessor.get(IEditorService);
const changesViewService = accessor.get(IChangesViewService);
const sessionChanges = changesViewService.activeSessionChangesObs.get();
const changes = sessionChanges?.filter(change =>
resources.some(resource => isEqual(change.modifiedUri ?? change.originalUri, resource))
) ?? [];
await Promise.all(changes.map(change => editorService.openEditor({
original: { resource: change.originalUri },
modified: { resource: change.modifiedUri }
})));
}
}
registerAction2(OpenChangesAction);
const openSingleFileDiffEnabled = ContextKeyExpr.equals(`config.${SESSIONS_CHANGES_OPEN_SINGLE_FILE_DIFF_SETTING}`, true);
class OpenFileAction extends Action2 {
static readonly ID = 'workbench.action.agentSessions.openFile';
constructor() {
super({
id: OpenFileAction.ID,
title: localize2('openFile', "Open File"),
icon: Codicon.goToFile,
f1: false,
menu: [
// When opening a file already shows a single file diff, the "Open
// Changes" alt action is redundant and is therefore omitted.
{
id: MenuId.AgentsChangeInlineToolbar,
group: 'navigation',
order: 1,
when: ContextKeyExpr.and(
IsSessionsWindowContext,
ChangesContextKeys.ChangeKind.isEqualTo('file'),
openSingleFileDiffEnabled)
},
// Default behavior: the alt action ("Open Changes") opens a diff
// editor for the selected change(s).
{
id: MenuId.AgentsChangeInlineToolbar,
group: 'navigation',
order: 1,
alt: {
id: OpenChangesAction.ID,
title: localize2('openChanges', "Open Changes"),
icon: Codicon.gitCompare,
},
when: ContextKeyExpr.and(
IsSessionsWindowContext,
ChangesContextKeys.ChangeKind.isEqualTo('file'),
openSingleFileDiffEnabled.negate())
}
]
});
}
async run(accessor: ServicesAccessor, _sessionResource: URI, _ref: string, ...resources: URI[]): Promise<void> {
const editorService = accessor.get(IEditorService);
await Promise.all(resources.map(resource => editorService.openEditor({ resource })));
}
}
registerAction2(OpenFileAction);