Skip to content

Commit

Permalink
add/expose keybindings for chat overlay commands
Browse files Browse the repository at this point in the history
  • Loading branch information
jrieken committed Nov 5, 2024
1 parent b54e52d commit 440a827
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 121 deletions.
6 changes: 3 additions & 3 deletions src/vs/base/browser/ui/actionbar/actionViewItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ export interface IActionViewItemOptions extends IBaseActionViewItemOptions {
icon?: boolean;
label?: boolean;
keybinding?: string | null;
keybindingNotRenderedWithLabel?: boolean;
toggleStyles?: IToggleStyles;
}

Expand Down Expand Up @@ -300,7 +301,7 @@ export class ActionViewItem extends BaseActionViewItem {
this.label = label;
this.element.appendChild(label);

if (this.options.label && this.options.keybinding) {
if (this.options.label && this.options.keybinding && !this.options.keybindingNotRenderedWithLabel) {
const kbLabel = document.createElement('span');
kbLabel.classList.add('keybinding');
kbLabel.textContent = this.options.keybinding;
Expand Down Expand Up @@ -365,9 +366,8 @@ export class ActionViewItem extends BaseActionViewItem {
if (this.action.tooltip) {
title = this.action.tooltip;

} else if (!this.options.label && this.action.label && this.options.icon) {
} else if (this.action.label) {
title = this.action.label;

if (this.options.keybinding) {
title = nls.localize({ key: 'titleLabel', comment: ['action title', 'action keybinding'] }, "{0} ({1})", title, this.options.keybinding);
}
Expand Down
1 change: 1 addition & 0 deletions src/vs/platform/actions/common/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ export class MenuId {
static readonly ChatInput = new MenuId('ChatInput');
static readonly ChatInputSide = new MenuId('ChatInputSide');
static readonly ChatEditingWidgetToolbar = new MenuId('ChatEditingWidgetToolbar');
static readonly ChatEditingEditorContent = new MenuId('ChatEditingEditorContent');
static readonly ChatEditingWidgetModifiedFilesToolbar = new MenuId('ChatEditingWidgetModifiedFilesToolbar');
static readonly ChatInlineResourceAnchorContext = new MenuId('ChatInlineResourceAnchorContext');
static readonly ChatInlineSymbolAnchorContext = new MenuId('ChatInlineSymbolAnchorContext');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,10 @@ export function registerNewChatActions() {
when: ChatContextKeys.editingParticipantRegistered,
group: 'a_chatEdit',
order: 1
}, {
id: MenuId.ChatEditingEditorContent,
group: 'navigate',
order: 1,
}],
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
Expand Down
87 changes: 77 additions & 10 deletions src/vs/workbench/contrib/chat/browser/chatEditorActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { localize2 } from '../../../../nls.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
import { CHAT_CATEGORY } from './actions/chatActions.js';
import { ChatEditorController, ctxHasEditorModification } from './chatEditorController.js';
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { IChatEditingService } from '../common/chatEditingService.js';
import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js';
import { hasUndecidedChatEditingResourceContextKey, IChatEditingService } from '../common/chatEditingService.js';
import { ChatContextKeys } from '../common/chatContextKeys.js';
import { isEqual } from '../../../../base/common/resources.js';
import { Range } from '../../../../editor/common/core/range.js';

abstract class NavigateAction extends Action2 {

Expand All @@ -35,23 +38,71 @@ abstract class NavigateAction extends Action2 {
weight: KeybindingWeight.EditorContrib,
when: ContextKeyExpr.and(ctxHasEditorModification, EditorContextKeys.focus),
},
f1: true
f1: true,
menu: {
id: MenuId.ChatEditingEditorContent,
group: 'navigate',
order: !next ? 2 : 3,
}
});
}

override run(accessor: ServicesAccessor) {

const editor = accessor.get(IEditorService).activeTextEditorControl;
const chatEditingService = accessor.get(IChatEditingService);
const editorService = accessor.get(IEditorService);

if (!isCodeEditor(editor)) {
const editor = editorService.activeTextEditorControl;
if (!isCodeEditor(editor) || !editor.hasModel()) {
return;
}

if (this.next) {
ChatEditorController.get(editor)?.revealNext();
} else {
ChatEditorController.get(editor)?.revealPrevious();
const session = chatEditingService.currentEditingSession;
if (!session) {
return;
}

const ctrl = ChatEditorController.get(editor);
if (!ctrl) {
return;
}

const done = this.next
? ctrl.revealNext(true)
: ctrl.revealPrevious(true);

if (done) {
return;
}

const entries = session.entries.get();
const idx = entries.findIndex(e => isEqual(e.modifiedURI, editor.getModel().uri));
if (idx < 0) {
return;
}

const newIdx = (idx + (this.next ? 1 : -1) + entries.length) % entries.length;
if (idx === newIdx) {
// wrap inside the same file
if (this.next) {
ctrl.revealNext(false);
} else {
ctrl.revealPrevious(false);
}
return;
}

const entry = entries[newIdx];
const change = entry.diffInfo.get().changes.at(0);

return editorService.openEditor({
resource: entry.modifiedURI,
options: {
selection: change && Range.fromPositions({ lineNumber: change.original.startLineNumber, column: 1 }),
revealIfOpened: false,
revealIfVisible: false,
}
}, ACTIVE_GROUP);
}
}

Expand All @@ -65,11 +116,27 @@ abstract class AcceptDiscardAction extends Action2 {
title: accept
? localize2('accept', 'Accept Chat Edit')
: localize2('discard', 'Discard Chat Edit'),
shortTitle: accept
? localize2('accept2', 'Accept')
: localize2('discard2', 'Discard'),
category: CHAT_CATEGORY,
precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey),
icon: accept
? Codicon.check
: Codicon.discard,
f1: true,
keybinding: {
when: EditorContextKeys.focus,
weight: KeybindingWeight.WorkbenchContrib,
primary: accept
? KeyMod.CtrlCmd | KeyCode.Enter
: KeyMod.CtrlCmd | KeyCode.Backspace
},
menu: {
id: MenuId.ChatEditingEditorContent,
group: 'a_resolve',
order: accept ? 0 : 1,
}
});
}

Expand Down
153 changes: 45 additions & 108 deletions src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,18 @@

import './media/chatEditorOverlay.css';
import { DisposableStore } from '../../../../base/common/lifecycle.js';
import { autorun, observableFromEvent } from '../../../../base/common/observable.js';
import { autorun, observableFromEvent, observableValue } from '../../../../base/common/observable.js';
import { isEqual } from '../../../../base/common/resources.js';
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js';
import { IEditorContribution } from '../../../../editor/common/editorCommon.js';
import { WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
import { MenuWorkbenchToolBar, WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { ChatEditingSessionState, IChatEditingService, IModifiedFileEntry, WorkingSetEntryState } from '../common/chatEditingService.js';
import { Separator, toAction } from '../../../../base/common/actions.js';
import { localize } from '../../../../nls.js';
import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js';
import { Range } from '../../../../editor/common/core/range.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { ChatEditorController } from './chatEditorController.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
import { EDITS_VIEW_ID } from './chat.js';
import { MenuId } from '../../../../platform/actions/common/actions.js';
import { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { Codicon } from '../../../../base/common/codicons.js';

class ChatEditorOverlayWidget implements IOverlayWidget {

Expand All @@ -33,20 +28,48 @@ class ChatEditorOverlayWidget implements IOverlayWidget {
private _isAdded: boolean = false;
private readonly _showStore = new DisposableStore();

private readonly _entry = observableValue<IModifiedFileEntry | undefined>(this, undefined);

constructor(
private readonly _editor: ICodeEditor,
@IEditorService private readonly _editorService: IEditorService,
@IViewsService private readonly _viewsService: IViewsService,
@IInstantiationService instaService: IInstantiationService,
@IKeybindingService keybindingService: IKeybindingService,
) {
this._domNode = document.createElement('div');
this._domNode.classList.add('chat-editor-overlay-widget');

this._toolbar = instaService.createInstance(WorkbenchToolBar, this._domNode, {
this._toolbar = instaService.createInstance(MenuWorkbenchToolBar, this._domNode, MenuId.ChatEditingEditorContent, {
telemetrySource: 'chatEditor.overlayToolbar',
toolbarOptions: {
primaryGroup: () => true,
useSeparatorsInPrimaryActions: true
},
menuOptions: { renderShortTitle: true },
actionViewItemProvider: (action, options) => {
if (action.id === 'accept' || action.id === 'discard') {
return new ActionViewItem(undefined, action, { ...options, label: true, icon: false });
if (action.id === 'workbench.action.chat.openEditSession') {

const that = this;

return new class extends ActionViewItem {
constructor() {
super(undefined, action, { ...options });
this._store.add(autorun(r => {
const entry = that._entry.read(r);
entry?.isCurrentlyBeingModified.read(r);
this.updateClass();
}));
}
protected override getClass(): string | undefined {
const entry = that._entry.get();
const busy = entry?.isCurrentlyBeingModified.get();
return busy
? ThemeIcon.asClassName(ThemeIcon.modify(Codicon.loading, 'spin'))
: action.class;
}
};
}
if (action.id === 'chatEditor.action.accept' || action.id === 'chatEditor.action.reject') {
return new ActionViewItem(undefined, action, { ...options, icon: false, label: true, keybindingNotRenderedWithLabel: true });
}
return undefined;
}
Expand All @@ -71,95 +94,9 @@ class ChatEditorOverlayWidget implements IOverlayWidget {
return { preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER };
}

show(entry: IModifiedFileEntry, prevEntry: IModifiedFileEntry, nextEntry: IModifiedFileEntry) {

this._showStore.clear();
show(entry: IModifiedFileEntry) {

const ctrl = ChatEditorController.get(this._editor);
if (!ctrl) {
return;
}

const navigate = (next: boolean) => {
const didRevealWithin = next ? ctrl.revealNext(true) : ctrl.revealPrevious(true);
if (!didRevealWithin) {
reveal(next ? nextEntry : prevEntry);
}
};

const reveal = (entry: IModifiedFileEntry) => {

const change = entry.diffInfo.get().changes.at(0);
return this._editorService.openEditor({
resource: entry.modifiedURI,
options: {
selection: change && Range.fromPositions({ lineNumber: change.original.startLineNumber, column: 1 }),
revealIfOpened: false,
revealIfVisible: false,
}
}, ACTIVE_GROUP);
};

this._showStore.add(autorun(r => {

const busy = entry.isCurrentlyBeingModified.read(r);
const modified = entry.state.read(r) === WorkingSetEntryState.Modified;

this._domNode.classList.toggle('busy', busy);

const groups = [[
toAction({
id: 'open',
label: localize('open', 'Open Chat Edit'),
class: ThemeIcon.asClassName(busy
? ThemeIcon.modify(Codicon.loading, 'spin')
: Codicon.goToEditingSession),
run: async () => {
await this._viewsService.openView(EDITS_VIEW_ID);
}
}),
toAction({
id: 'accept',
label: localize('accept', 'Accept'),
tooltip: localize('acceptTooltip', 'Accept Chat Edits'),
class: ThemeIcon.asClassName(Codicon.check),
enabled: !busy && modified,
run: () => {
entry.accept(undefined);
reveal(nextEntry);
}
}),
toAction({
id: 'discard',
label: localize('discard', 'Discard'),
tooltip: localize('discardTooltip', 'Discard Chat Edits'),
class: ThemeIcon.asClassName(Codicon.discard),
enabled: !busy && modified,
run: () => {
entry.reject(undefined);
reveal(nextEntry);
}
}),
], [
toAction({
id: 'prev',
label: localize('prev', 'Previous Entry'),
class: ThemeIcon.asClassName(Codicon.arrowUp),
enabled: entry !== prevEntry,
run: () => navigate(false)
}),
toAction({
id: 'next',
label: localize('next', 'Next Entry'),
class: ThemeIcon.asClassName(Codicon.arrowDown),
enabled: entry !== nextEntry,
run: () => navigate(true)
})
]];

const actions = Separator.join(...groups);
this._toolbar.setActions(actions);
}));
this._entry.set(entry, undefined);

if (!this._isAdded) {
this._editor.addOverlayWidget(this);
Expand All @@ -168,6 +105,9 @@ class ChatEditorOverlayWidget implements IOverlayWidget {
}

hide() {

this._entry.set(undefined, undefined);

if (this._isAdded) {
this._editor.removeOverlayWidget(this);
this._isAdded = false;
Expand Down Expand Up @@ -222,10 +162,7 @@ export class ChatEditorOverlayController implements IEditorContribution {
}

const entry = entries[idx];
const prevEntry = entries[(idx - 1 + entries.length) % entries.length];
const nextEntry = entries[(idx + 1) % entries.length];

widget.show(entry, prevEntry, nextEntry);
widget.show(entry);

}));
}
Expand Down

0 comments on commit 440a827

Please sign in to comment.