Skip to content

Commit 916afeb

Browse files
authored
add commands to navigate through edits (microsoft#230791)
1 parent 066d55d commit 916afeb

File tree

3 files changed

+154
-2
lines changed

3 files changed

+154
-2
lines changed

src/vs/workbench/contrib/chat/browser/chat.contribution.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ import { EditorContributionInstantiation, registerEditorContribution } from '../
7373
import { ChatEditorController } from './chatEditorController.js';
7474
import { LanguageModelToolsService } from './languageModelToolsService.js';
7575
import { ChatEditorSaving } from './chatEditorSaving.js';
76+
import { registerChatEditorActions } from './chatEditorActions.js';
77+
7678

7779
// Register configuration
7880
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
@@ -298,6 +300,7 @@ registerMoveActions();
298300
registerNewChatActions();
299301
registerChatContextActions();
300302
registerChatDeveloperActions();
303+
registerChatEditorActions();
301304

302305
registerEditorFeature(ChatPasteProvidersFeature);
303306
registerEditorContribution(ChatEditorController.ID, ChatEditorController, EditorContributionInstantiation.Eventually);
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
6+
import { localize2 } from '../../../../nls.js';
7+
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
8+
import { Codicon } from '../../../../base/common/codicons.js';
9+
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
10+
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
11+
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
12+
import { CHAT_CATEGORY } from './actions/chatActions.js';
13+
import { ChatEditorController, ctxHasEditorModification } from './chatEditorController.js';
14+
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
15+
import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';
16+
import { IEditorService } from '../../../services/editor/common/editorService.js';
17+
18+
abstract class NavigateAction extends Action2 {
19+
20+
constructor(readonly next: boolean) {
21+
super({
22+
id: next
23+
? 'chat.action.navigateNext'
24+
: 'chat.action.navigatePrevious',
25+
title: next
26+
? localize2('next', 'Go to Next Chat Edit')
27+
: localize2('prev', 'Go to Previous Chat Edit'),
28+
category: CHAT_CATEGORY,
29+
icon: next ? Codicon.arrowDown : Codicon.arrowUp,
30+
keybinding: {
31+
primary: next
32+
? KeyMod.Alt | KeyCode.F5
33+
: KeyMod.Alt | KeyMod.Shift | KeyCode.F5,
34+
weight: KeybindingWeight.EditorContrib,
35+
when: ContextKeyExpr.and(ctxHasEditorModification, EditorContextKeys.focus),
36+
},
37+
f1: true,
38+
menu: {
39+
id: MenuId.EditorTitle,
40+
group: 'navigation',
41+
order: next ? -100 : -101,
42+
when: ctxHasEditorModification
43+
}
44+
});
45+
}
46+
47+
override run(accessor: ServicesAccessor) {
48+
49+
const editor = accessor.get(IEditorService).activeTextEditorControl;
50+
51+
if (!isCodeEditor(editor)) {
52+
return;
53+
}
54+
55+
if (this.next) {
56+
ChatEditorController.get(editor)?.revealNext();
57+
} else {
58+
ChatEditorController.get(editor)?.revealPrevious();
59+
}
60+
}
61+
62+
}
63+
64+
65+
export function registerChatEditorActions() {
66+
registerAction2(class extends NavigateAction { constructor() { super(true); } });
67+
registerAction2(class extends NavigateAction { constructor() { super(false); } });
68+
}

src/vs/workbench/contrib/chat/browser/chatEditorController.ts

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,56 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { binarySearch, coalesceInPlace } from '../../../../base/common/arrays.js';
67
import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
78
import { Constants } from '../../../../base/common/uint.js';
89
import { ICodeEditor, IViewZone } from '../../../../editor/browser/editorBrowser.js';
910
import { LineSource, renderLines, RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js';
1011
import { diffAddDecoration, diffDeleteDecoration, diffWholeLineAddDecoration } from '../../../../editor/browser/widget/diffEditor/registrations.contribution.js';
1112
import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
13+
import { Range } from '../../../../editor/common/core/range.js';
1214
import { IDocumentDiff } from '../../../../editor/common/diff/documentDiffProvider.js';
13-
import { IEditorContribution } from '../../../../editor/common/editorCommon.js';
15+
import { IEditorContribution, ScrollType } from '../../../../editor/common/editorCommon.js';
1416
import { IModelDeltaDecoration, ITextModel } from '../../../../editor/common/model.js';
1517
import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js';
1618
import { InlineDecoration, InlineDecorationType } from '../../../../editor/common/viewModel.js';
1719
import { IChatEditingService, IChatEditingSession, IModifiedFileEntry, WorkingSetEntryState } from '../common/chatEditingService.js';
20+
import { localize } from '../../../../nls.js';
21+
import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
22+
23+
export const ctxHasEditorModification = new RawContextKey<boolean>('chat.hasEditorModifications', undefined, localize('chat.hasEditorModifications', "The current editor contains chat modifications"));
1824

1925
export class ChatEditorController extends Disposable implements IEditorContribution {
2026

2127
public static readonly ID = 'editor.contrib.chatEditorController';
28+
2229
private readonly _sessionStore = this._register(new DisposableStore());
2330
private readonly _decorations = this._editor.createDecorationsCollection();
2431
private _viewZones: string[] = [];
32+
private readonly _ctxHasEditorModification: IContextKey<boolean>;
33+
34+
static get(editor: ICodeEditor): ChatEditorController | null {
35+
const controller = editor.getContribution<ChatEditorController>(ChatEditorController.ID);
36+
return controller;
37+
}
2538

2639
constructor(
2740
private readonly _editor: ICodeEditor,
2841
@IChatEditingService private readonly _chatEditingService: IChatEditingService,
29-
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService
42+
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
43+
@IContextKeyService contextKeyService: IContextKeyService,
3044
) {
3145
super();
3246
this._register(this._editor.onDidChangeModel(() => this._update()));
3347
this._register(this._chatEditingService.onDidChangeEditingSession(() => this._updateSessionDecorations()));
3448
this._register(toDisposable(() => this._clearRendering()));
49+
50+
this._ctxHasEditorModification = ctxHasEditorModification.bindTo(contextKeyService);
51+
}
52+
53+
override dispose(): void {
54+
this._clearRendering();
55+
super.dispose();
3556
}
3657

3758
private _update(): void {
@@ -88,6 +109,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut
88109
return;
89110
}
90111

112+
this._ctxHasEditorModification.set(true);
91113
this._updateWithDiff(model, entry, diff);
92114
});
93115
}
@@ -107,6 +129,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut
107129
});
108130
this._viewZones = [];
109131
this._decorations.clear();
132+
this._ctxHasEditorModification.reset();
110133
}
111134

112135
private _updateWithDiff(model: ITextModel, entry: IModifiedFileEntry, diff: IDocumentDiff | null): void {
@@ -166,4 +189,62 @@ export class ChatEditorController extends Disposable implements IEditorContribut
166189
this._decorations.set(modifiedDecorations);
167190
});
168191
}
192+
193+
revealNext() {
194+
this._reveal(true);
195+
}
196+
197+
revealPrevious() {
198+
this._reveal(false);
199+
}
200+
201+
private _reveal(next: boolean) {
202+
const position = this._editor.getPosition();
203+
if (!position) {
204+
return;
205+
}
206+
207+
const decorations: (Range | undefined)[] = this._decorations
208+
.getRanges()
209+
.sort((a, b) => Range.compareRangesUsingStarts(a, b));
210+
211+
// TODO@jrieken this is slow and should be done smarter, e.g being able to read
212+
// only whole range decorations because the goal is to go from change to change, skipping
213+
// over word level changes
214+
for (let i = 0; i < decorations.length; i++) {
215+
const decoration = decorations[i];
216+
for (let j = 0; j < decorations.length; j++) {
217+
if (i !== j && decoration && decorations[j]?.containsRange(decoration)) {
218+
decorations[i] = undefined;
219+
break;
220+
}
221+
}
222+
}
223+
224+
coalesceInPlace(decorations);
225+
226+
if (decorations.length === 0) {
227+
return;
228+
}
229+
230+
let idx = binarySearch(decorations, Range.fromPositions(position), Range.compareRangesUsingStarts);
231+
if (idx < 0) {
232+
idx = ~idx;
233+
}
234+
235+
let target: number;
236+
if (decorations[idx]?.containsPosition(position)) {
237+
target = idx + (next ? 1 : -1);
238+
} else {
239+
target = next ? idx : idx - 1;
240+
}
241+
242+
target = (target + decorations.length) % decorations.length;
243+
244+
245+
const targetPosition = decorations[target].getStartPosition();
246+
this._editor.setPosition(targetPosition);
247+
this._editor.revealPosition(targetPosition, ScrollType.Smooth);
248+
this._editor.focus();
249+
}
169250
}

0 commit comments

Comments
 (0)