Skip to content

Commit d3147d0

Browse files
authored
improve diff for /edits (#237801)
* improve diff for /edits * no double-diff for chat modified entries * align whitespace property * overflow logic for overlay widget * fix compiler
1 parent 06c07d2 commit d3147d0

File tree

5 files changed

+96
-46
lines changed

5 files changed

+96
-46
lines changed

src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { RunOnceScheduler } from '../../../../../base/common/async.js';
77
import { Emitter } from '../../../../../base/common/event.js';
88
import { Disposable, IReference, toDisposable } from '../../../../../base/common/lifecycle.js';
99
import { Schemas } from '../../../../../base/common/network.js';
10-
import { IObservable, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js';
10+
import { autorun, IObservable, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js';
1111
import { themeColorFromId } from '../../../../../base/common/themables.js';
1212
import { URI } from '../../../../../base/common/uri.js';
1313
import { EditOperation, ISingleEditOperation } from '../../../../../editor/common/core/editOperation.js';
@@ -26,7 +26,9 @@ import { IModelService } from '../../../../../editor/common/services/model.js';
2626
import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js';
2727
import { IModelContentChangedEvent } from '../../../../../editor/common/textModelEvents.js';
2828
import { localize } from '../../../../../nls.js';
29+
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
2930
import { IFileService } from '../../../../../platform/files/common/files.js';
31+
import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js';
3032
import { editorSelectionBackground } from '../../../../../platform/theme/common/colorRegistry.js';
3133
import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js';
3234
import { SaveReason } from '../../../../common/editor.js';
@@ -130,6 +132,8 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie
130132
return this._telemetryInfo.requestId;
131133
}
132134

135+
private readonly _diffTrimWhitespace: IObservable<boolean>;
136+
133137
constructor(
134138
resourceRef: IReference<IResolvedTextEditorModel>,
135139
private readonly _multiDiffEntryDelegate: { collapse: (transaction: ITransaction | undefined) => void },
@@ -139,6 +143,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie
139143
@IModelService modelService: IModelService,
140144
@ITextModelService textModelService: ITextModelService,
141145
@ILanguageService languageService: ILanguageService,
146+
@IConfigurationService configService: IConfigurationService,
142147
@IChatService private readonly _chatService: IChatService,
143148
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
144149
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,
@@ -186,6 +191,12 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie
186191
this._register(toDisposable(() => {
187192
this._clearCurrentEditLineDecoration();
188193
}));
194+
195+
this._diffTrimWhitespace = observableConfigValue('diffEditor.ignoreTrimWhitespace', true, configService);
196+
this._register(autorun(r => {
197+
this._diffTrimWhitespace.read(r);
198+
this._updateDiffInfoSeq();
199+
}));
189200
}
190201

191202
private _clearCurrentEditLineDecoration() {
@@ -413,10 +424,12 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie
413424
const docVersionNow = this.doc.getVersionId();
414425
const snapshotVersionNow = this.docSnapshot.getVersionId();
415426

427+
const ignoreTrimWhitespace = this._diffTrimWhitespace.get();
428+
416429
const diff = await this._editorWorkerService.computeDiff(
417430
this.docSnapshot.uri,
418431
this.doc.uri,
419-
{ computeMoves: true, ignoreTrimWhitespace: false, maxComputationTimeMs: 3000 },
432+
{ ignoreTrimWhitespace, computeMoves: false, maxComputationTimeMs: 3000 },
420433
'advanced'
421434
);
422435

src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ILanguageService } from '../../../../../editor/common/languages/languag
99
import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js';
1010
import { IModelService } from '../../../../../editor/common/services/model.js';
1111
import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js';
12+
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
1213
import { IFileService } from '../../../../../platform/files/common/files.js';
1314
import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js';
1415
import { SaveReason } from '../../../../common/editor.js';
@@ -33,8 +34,9 @@ export class ChatEditingModifiedNotebookEntry extends ChatEditingModifiedFileEnt
3334
@IEditorWorkerService _editorWorkerService: IEditorWorkerService,
3435
@IUndoRedoService _undoRedoService: IUndoRedoService,
3536
@IFileService _fileService: IFileService,
37+
@IConfigurationService configService: IConfigurationService
3638
) {
37-
super(resourceRef, _multiDiffEntryDelegate, _telemetryInfo, kind, initialContent, modelService, textModelService, languageService, _chatService, _editorWorkerService, _undoRedoService, _fileService);
39+
super(resourceRef, _multiDiffEntryDelegate, _telemetryInfo, kind, initialContent, modelService, textModelService, languageService, configService, _chatService, _editorWorkerService, _undoRedoService, _fileService);
3840
this.resolveTextFileEditorModel = resourceRef.object as IResolvedTextFileEditorModel;
3941
}
4042

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

+60-40
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import './media/chatEditorController.css';
77
import { addStandardDisposableListener, getTotalWidth } from '../../../../base/browser/dom.js';
88
import { Disposable, DisposableStore, dispose, toDisposable } from '../../../../base/common/lifecycle.js';
9-
import { autorun, autorunWithStore, derived, IObservable, observableFromEvent, observableValue, transaction } from '../../../../base/common/observable.js';
9+
import { autorun, autorunWithStore, derived, IObservable, observableFromEvent, observableValue } from '../../../../base/common/observable.js';
1010
import { themeColorFromId } from '../../../../base/common/themables.js';
1111
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IOverlayWidgetPositionCoordinates, IViewZone, MouseTargetType } from '../../../../editor/browser/editorBrowser.js';
1212
import { LineSource, renderLines, RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js';
@@ -80,9 +80,10 @@ export class ChatEditorController extends Disposable implements IEditorContribut
8080
this._ctxHasEditorModification = ctxHasEditorModification.bindTo(contextKeyService);
8181
this._ctxRequestInProgress = ctxHasRequestInProgress.bindTo(contextKeyService);
8282

83-
const fontInfoObs = observableCodeEditor(this._editor).getOption(EditorOption.fontInfo);
84-
const lineHeightObs = observableCodeEditor(this._editor).getOption(EditorOption.lineHeight);
85-
const modelObs = observableCodeEditor(this._editor).model;
83+
const editorObs = observableCodeEditor(this._editor);
84+
const fontInfoObs = editorObs.getOption(EditorOption.fontInfo);
85+
const lineHeightObs = editorObs.getOption(EditorOption.lineHeight);
86+
const modelObs = editorObs.model;
8687

8788

8889
this._store.add(autorun(r => {
@@ -112,13 +113,13 @@ export class ChatEditorController extends Disposable implements IEditorContribut
112113
const currentEditorEntry = entryForEditor.read(r);
113114

114115
if (!currentEditorEntry) {
115-
this._clearRendering();
116+
this._clear();
116117
didReval = false;
117118
return;
118119
}
119120

120121
if (this._editor.getOption(EditorOption.inDiffEditor) && !_instantiationService.invokeFunction(isDiffEditorForEntry, currentEditorEntry.entry, this._editor)) {
121-
this._clearRendering();
122+
this._clear();
122123
return;
123124
}
124125

@@ -145,7 +146,16 @@ export class ChatEditorController extends Disposable implements IEditorContribut
145146
lineHeightObs.read(r);
146147

147148
const diff = entry?.diffInfo.read(r);
148-
this._updateWithDiff(entry, diff);
149+
150+
// Add line decorations (just markers, no UI) for diff navigation
151+
this._updateDiffLineDecorations(diff);
152+
153+
// Add diff decoration to the UI (unless in diff editor)
154+
if (!this._editor.getOption(EditorOption.inDiffEditor)) {
155+
this._updateDiffRendering(entry, diff);
156+
} else {
157+
this._clearDiffRendering();
158+
}
149159

150160
if (!didReval && !diff.identical) {
151161
didReval = true;
@@ -199,11 +209,19 @@ export class ChatEditorController extends Disposable implements IEditorContribut
199209
}
200210

201211
override dispose(): void {
202-
this._clearRendering();
212+
this._clear();
203213
super.dispose();
204214
}
205215

206-
private _clearRendering() {
216+
private _clear() {
217+
this._clearDiffRendering();
218+
this._diffLineDecorations.clear();
219+
this._currentChangeIndex.set(undefined, undefined);
220+
this._currentEntryIndex.set(undefined, undefined);
221+
this._ctxHasEditorModification.reset();
222+
}
223+
224+
private _clearDiffRendering() {
207225
this._editor.changeViewZones((viewZoneChangeAccessor) => {
208226
for (const id of this._viewZones) {
209227
viewZoneChangeAccessor.removeZone(id);
@@ -212,18 +230,11 @@ export class ChatEditorController extends Disposable implements IEditorContribut
212230
this._viewZones = [];
213231
this._diffHunksRenderStore.clear();
214232
this._diffVisualDecorations.clear();
215-
this._diffLineDecorations.clear();
216-
this._ctxHasEditorModification.reset();
217-
transaction(tx => {
218-
this._currentEntryIndex.set(undefined, tx);
219-
this._currentChangeIndex.set(undefined, tx);
220-
});
221233
this._scrollLock = false;
222234
}
223235

224-
private _updateWithDiff(entry: IModifiedFileEntry, diff: IDocumentDiff): void {
236+
private _updateDiffRendering(entry: IModifiedFileEntry, diff: IDocumentDiff): void {
225237

226-
this._ctxHasEditorModification.set(!diff.identical);
227238
const originalModel = entry.originalModel;
228239

229240
const chatDiffAddDecoration = ModelDecorationOptions.createDynamic({
@@ -255,7 +266,6 @@ export class ChatEditorController extends Disposable implements IEditorContribut
255266
}
256267
this._viewZones = [];
257268
const modifiedVisualDecorations: IModelDeltaDecoration[] = [];
258-
const modifiedLineDecorations: IModelDeltaDecoration[] = [];
259269
const mightContainNonBasicASCII = originalModel.mightContainNonBasicASCII();
260270
const mightContainRTL = originalModel.mightContainRTL();
261271
const renderOptions = RenderOptions.fromEditor(this._editor);
@@ -344,16 +354,9 @@ export class ChatEditorController extends Disposable implements IEditorContribut
344354
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges
345355
}
346356
});
347-
348-
// Add line decorations for diff navigation
349-
modifiedLineDecorations.push({
350-
range: diffEntry.modified.toInclusiveRange() ?? new Range(diffEntry.modified.startLineNumber, 1, diffEntry.modified.startLineNumber, Number.MAX_SAFE_INTEGER),
351-
options: ChatEditorController._diffLineDecorationData
352-
});
353357
}
354358

355359
this._diffVisualDecorations.set(modifiedVisualDecorations);
356-
this._diffLineDecorations.set(modifiedLineDecorations);
357360
});
358361

359362
const diffHunkDecoCollection = this._editor.createDecorationsCollection(diffHunkDecorations);
@@ -428,6 +431,20 @@ export class ChatEditorController extends Disposable implements IEditorContribut
428431
}));
429432
}
430433

434+
private _updateDiffLineDecorations(diff: IDocumentDiff): void {
435+
this._ctxHasEditorModification.set(!diff.identical);
436+
437+
const modifiedLineDecorations: IModelDeltaDecoration[] = [];
438+
439+
for (const diffEntry of diff.changes) {
440+
modifiedLineDecorations.push({
441+
range: diffEntry.modified.toInclusiveRange() ?? new Range(diffEntry.modified.startLineNumber, 1, diffEntry.modified.startLineNumber, Number.MAX_SAFE_INTEGER),
442+
options: ChatEditorController._diffLineDecorationData
443+
});
444+
}
445+
this._diffLineDecorations.set(modifiedLineDecorations);
446+
}
447+
431448
unlockScroll(): void {
432449
this._scrollLock = false;
433450
}
@@ -533,6 +550,12 @@ export class ChatEditorController extends Disposable implements IEditorContribut
533550
if (!this._editor.hasModel()) {
534551
return;
535552
}
553+
554+
const entry = this._chatEditingService.currentEditingSessionObs.get()?.getEntry(this._editor.getModel().uri);
555+
if (!entry) {
556+
return;
557+
}
558+
536559
const lineRelativeTop = this._editor.getTopForLineNumber(this._editor.getPosition().lineNumber) - this._editor.getScrollTop();
537560
let closestDistance = Number.MAX_VALUE;
538561

@@ -549,33 +572,30 @@ export class ChatEditorController extends Disposable implements IEditorContribut
549572
}
550573
}
551574

552-
if (!(widget instanceof DiffHunkWidget)) {
553-
return;
554-
}
555-
556-
557-
const lineNumber = widget.getStartLineNumber();
558-
const position = lineNumber ? new Position(lineNumber, 1) : undefined;
559575
let selection = this._editor.getSelection();
560-
if (position && !selection.containsPosition(position)) {
561-
selection = Selection.fromPositions(position);
576+
if (widget instanceof DiffHunkWidget) {
577+
const lineNumber = widget.getStartLineNumber();
578+
const position = lineNumber ? new Position(lineNumber, 1) : undefined;
579+
if (position && !selection.containsPosition(position)) {
580+
selection = Selection.fromPositions(position);
581+
}
562582
}
563583

564584
const isDiffEditor = this._editor.getOption(EditorOption.inDiffEditor);
565585

566586
if (isDiffEditor) {
567587
// normal EDITOR
568-
await this._editorService.openEditor({ resource: widget.entry.modifiedURI });
588+
await this._editorService.openEditor({ resource: entry.modifiedURI });
569589

570590
} else {
571591
// DIFF editor
572592
const defaultAgentName = this._chatAgentService.getDefaultAgent(ChatAgentLocation.EditingSession)?.fullName;
573593
const diffEditor = await this._editorService.openEditor({
574-
original: { resource: widget.entry.originalURI, options: { selection: undefined } },
575-
modified: { resource: widget.entry.modifiedURI, options: { selection } },
594+
original: { resource: entry.originalURI, options: { selection: undefined } },
595+
modified: { resource: entry.modifiedURI, options: { selection } },
576596
label: defaultAgentName
577-
? localize('diff.agent', '{0} (changes from {1})', basename(widget.entry.modifiedURI), defaultAgentName)
578-
: localize('diff.generic', '{0} (changes from chat)', basename(widget.entry.modifiedURI))
597+
? localize('diff.agent', '{0} (changes from {1})', basename(entry.modifiedURI), defaultAgentName)
598+
: localize('diff.generic', '{0} (changes from chat)', basename(entry.modifiedURI))
579599
});
580600

581601
if (diffEditor && diffEditor.input) {
@@ -586,7 +606,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut
586606

587607
// close diff editor when entry is decided
588608
const d = autorun(r => {
589-
const state = widget.entry.state.read(r);
609+
const state = entry.state.read(r);
590610
if (state === WorkingSetEntryState.Accepted || state === WorkingSetEntryState.Rejected) {
591611
d.dispose();
592612
this._editorService.closeEditor(editorIdent);

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

+17-3
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ import { ChatEditorController } from './chatEditorController.js';
2828
import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
2929
import { isDiffEditorForEntry } from './chatEditing/chatEditing.js';
3030
import './media/chatEditorOverlay.css';
31+
import { findDiffEditorContainingCodeEditor } from '../../../../editor/browser/widget/diffEditor/commands.js';
3132

3233
class ChatEditorOverlayWidget implements IOverlayWidget {
3334

34-
readonly allowEditorOverflow = false;
35+
readonly allowEditorOverflow = true;
3536

3637
private readonly _domNode: HTMLElement;
3738
private readonly _progressNode: HTMLElement;
@@ -47,7 +48,7 @@ class ChatEditorOverlayWidget implements IOverlayWidget {
4748
constructor(
4849
private readonly _editor: ICodeEditor,
4950
@IEditorService editorService: IEditorService,
50-
@IInstantiationService instaService: IInstantiationService,
51+
@IInstantiationService private readonly _instaService: IInstantiationService,
5152
) {
5253
this._domNode = document.createElement('div');
5354
this._domNode.classList.add('chat-editor-overlay-widget');
@@ -62,7 +63,7 @@ class ChatEditorOverlayWidget implements IOverlayWidget {
6263
toolbarNode.classList.add('chat-editor-overlay-toolbar');
6364
this._domNode.appendChild(toolbarNode);
6465

65-
this._toolbar = instaService.createInstance(MenuWorkbenchToolBar, toolbarNode, MenuId.ChatEditingEditorContent, {
66+
this._toolbar = _instaService.createInstance(MenuWorkbenchToolBar, toolbarNode, MenuId.ChatEditingEditorContent, {
6667
telemetrySource: 'chatEditor.overlayToolbar',
6768
hiddenItemStrategy: HiddenItemStrategy.Ignore,
6869
toolbarOptions: {
@@ -228,6 +229,19 @@ class ChatEditorOverlayWidget implements IOverlayWidget {
228229
this._navigationBearings.set({ changeCount: changes, activeIdx, entriesCount: entries.length }, undefined);
229230
}));
230231

232+
233+
const editorWithObs = observableFromEvent(this._editor.onDidLayoutChange, () => {
234+
const diffEditor = this._instaService.invokeFunction(findDiffEditorContainingCodeEditor, this._editor);
235+
return diffEditor
236+
? diffEditor.getOriginalEditor().getLayoutInfo().contentWidth + diffEditor.getModifiedEditor().getLayoutInfo().contentWidth
237+
: this._editor.getLayoutInfo().contentWidth;
238+
});
239+
240+
this._showStore.add(autorun(r => {
241+
const width = editorWithObs.read(r);
242+
this._domNode.style.maxWidth = `${width - 20}px`;
243+
}));
244+
231245
if (!this._isAdded) {
232246
this._editor.addOverlayWidget(this);
233247
this._isAdded = true;

src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
align-items: center;
1414
z-index: 10;
1515
box-shadow: 0 2px 8px var(--vscode-widget-shadow);
16+
overflow: hidden;
1617
}
1718

1819
@keyframes pulse {

0 commit comments

Comments
 (0)