use diff information to size and crop live previews views (#180341)

also fixes https://github.com/microsoft/vscode-internalbacklog/issues/3874
This commit is contained in:
Johannes Rieken 2023-04-20 10:49:33 +02:00 committed by GitHub
parent c28ca0b1e8
commit dbf4b14dfa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 173 additions and 90 deletions

View file

@ -63,6 +63,10 @@ export class LineRangeMapping {
public toString(): string {
return `{${this.originalRange.toString()}->${this.modifiedRange.toString()}}`;
}
public get changedLineCount() {
return Math.max(this.originalRange.length, this.modifiedRange.length);
}
}
/**

1
src/vs/monaco.d.ts vendored
View file

@ -2431,6 +2431,7 @@ declare namespace monaco.editor {
readonly innerChanges: RangeMapping[] | undefined;
constructor(originalRange: LineRange, modifiedRange: LineRange, innerChanges: RangeMapping[] | undefined);
toString(): string;
get changedLineCount(): any;
}
/**

View file

@ -14,7 +14,6 @@ import { DisposableStore } from 'vs/base/common/lifecycle';
import { LRUCache } from 'vs/base/common/map';
import { isEqual } from 'vs/base/common/resources';
import { StopWatch } from 'vs/base/common/stopwatch';
import { splitLines } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import 'vs/css!./interactiveEditor';
import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser';
@ -23,11 +22,12 @@ import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { IEditorContribution, IEditorDecorationsCollection, ScrollType } from 'vs/editor/common/editorCommon';
import { LanguageSelector } from 'vs/editor/common/languageSelector';
import { CompletionContext, CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult, TextEdit } from 'vs/editor/common/languages';
import { ICursorStateComputer, IModelDecorationOptions, IModelDeltaDecoration, ITextModel, IValidEditOperation } from 'vs/editor/common/model';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { ModelDecorationOptions, createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { IModelService } from 'vs/editor/common/services/model';
@ -381,15 +381,18 @@ export class InteractiveEditorController implements IEditorContribution {
// CANCEL when input changes
this._editor.onDidChangeModel(this.cancelSession, this, store);
// REposition the zone widget whenever the block decoration changes
let lastPost: Position | undefined;
wholeRangeDecoration.onDidChange(e => {
const range = wholeRangeDecoration.getRange(0);
if (range && (!lastPost || !lastPost.equals(range.getEndPosition()))) {
lastPost = range.getEndPosition();
this._zone.updatePosition(lastPost);
}
}, undefined, store);
if (editMode === 'live') {
// REposition the zone widget whenever the block decoration changes
let lastPost: Position | undefined;
wholeRangeDecoration.onDidChange(e => {
const range = wholeRangeDecoration.getRange(0);
if (range && (!lastPost || !lastPost.equals(range.getEndPosition()))) {
lastPost = range.getEndPosition();
this._zone.updatePosition(lastPost);
}
}, undefined, store);
}
let ignoreModelChanges = false;
this._editor.onDidChangeModelContent(e => {
@ -424,9 +427,15 @@ export class InteractiveEditorController implements IEditorContribution {
const roundStore = new DisposableStore();
store.add(roundStore);
const diffBaseModel = this._modelService.createModel(textModel.getValue(), { languageId: textModel.getLanguageId(), onDidChange: Event.None }, undefined, true);
store.add(diffBaseModel);
const diffZone = this._instaService.createInstance(InteractiveEditorDiffWidget, this._editor, diffBaseModel);
let textModel0Changes: LineRangeMapping[] | undefined;
const textModel0 = this._modelService.createModel(
createTextBufferFactoryFromSnapshot(textModel.createSnapshot()),
{ languageId: textModel.getLanguageId(), onDidChange: Event.None },
undefined, true
);
store.add(textModel0);
const diffZone = this._instaService.createInstance(InteractiveEditorDiffWidget, this._editor, textModel0);
do {
@ -439,13 +448,6 @@ export class InteractiveEditorController implements IEditorContribution {
break;
}
if (round > 1 && editMode === 'livePreview') {
if (!textModel.equalsTextBuffer(diffBaseModel.getTextBuffer())) {
diffZone.show(wholeRangeDecoration.getRange(0)!);
} else {
diffZone.hide();
}
}
// visuals: add block decoration
blockDecoration.set([{
@ -457,8 +459,21 @@ export class InteractiveEditorController implements IEditorContribution {
this._ctsRequest = new CancellationTokenSource(this._ctsSession.token);
this._historyOffset = -1;
const inputPromise = this._zone.getInput(wholeRange.getEndPosition(), placeholder, value, this._ctsRequest.token);
if (textModel0Changes && editMode === 'livePreview') {
const diffPosition = diffZone.getEndPositionForChanges(wholeRange, textModel0Changes);
if (diffPosition) {
const newInputPosition = diffPosition.delta(0, 1);
if (wholeRange.getEndPosition().isBefore(newInputPosition)) {
this._zone.updatePosition(newInputPosition);
}
}
diffZone.showDiff(
() => wholeRangeDecoration.getRange(0)!, // TODO@jrieken if it can be null it will be null
textModel0Changes
);
}
this._ctxLastFeedbackKind.reset();
// reveal the line after the whole range to ensure that the input box is visible
this._editor.revealPosition({ lineNumber: wholeRange.endLineNumber + 1, column: 1 }, ScrollType.Smooth);
@ -570,9 +585,17 @@ export class InteractiveEditorController implements IEditorContribution {
} else {
// make edits more minimal
const moreMinimalEdits = (await this._editorWorkerService.computeHumanReadableDiff(textModel.uri, editResponse.localEdits));
const moreMinimalEdits = (await this._editorWorkerService.computeMoreMinimalEdits(textModel.uri, editResponse.localEdits));
const editOperations = (moreMinimalEdits ?? editResponse.localEdits).map(edit => EditOperation.replace(Range.lift(edit.range), edit.text));
this._logService.trace('[IE] edits from PROVIDER and after making them MORE MINIMAL', provider.debugName, editResponse.localEdits, moreMinimalEdits);
const textModelNplus1 = this._modelService.createModel(createTextBufferFactoryFromSnapshot(textModel.createSnapshot()), null, undefined, true);
textModelNplus1.applyEdits(editOperations);
const diff = await this._editorWorkerService.computeDiff(textModel0.uri, textModelNplus1.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: 5000 }, 'advanced');
textModel0Changes = diff?.changes ?? undefined;
textModelNplus1.dispose();
try {
ignoreModelChanges = true;
@ -588,7 +611,7 @@ export class InteractiveEditorController implements IEditorContribution {
this._editor.pushUndoStop();
this._editor.executeEdits(
'interactive-editor',
(moreMinimalEdits ?? editResponse.localEdits).map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)),
editOperations,
cursorStateComputerAndInlineDiffCollection
);
this._editor.pushUndoStop();
@ -601,32 +624,22 @@ export class InteractiveEditorController implements IEditorContribution {
inlineDiffDecorations.update();
}
// line count
const lineSet = new Set<number>();
let addRemoveCount = 0;
for (const edit of moreMinimalEdits ?? editResponse.localEdits) {
const len2 = splitLines(edit.text).length - 1;
if (Range.isEmpty(edit.range) && len2 > 0) {
// insert lines
addRemoveCount += len2;
} else if (Range.isEmpty(edit.range) && edit.text.length === 0) {
// delete
addRemoveCount += edit.range.endLineNumber - edit.range.startLineNumber + 1;
} else {
// edit
for (let line = edit.range.startLineNumber; line <= edit.range.endLineNumber; line++) {
lineSet.add(line);
}
// summary message
let linesChanged = 0;
if (textModel0Changes) {
for (const change of textModel0Changes) {
linesChanged += change.changedLineCount;
}
}
const linesChanged = addRemoveCount + lineSet.size;
this._zone.widget.updateMessage(linesChanged === 1
? localize('lines.1', "Generated reply and changed 1 line.")
: localize('lines.N', "Generated reply and changed {0} lines.", linesChanged)
);
let message: string;
if (linesChanged === 0) {
message = localize('lines.0', "Generated reply");
} else if (linesChanged === 1) {
message = localize('lines.1', "Generated reply and changed 1 line.");
} else {
message = localize('lines.N', "Generated reply and changed {0} lines.", linesChanged);
}
this._zone.widget.updateMessage(message);
}

View file

@ -6,12 +6,10 @@
import { Dimension, h } from 'vs/base/browser/dom';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { assertType } from 'vs/base/common/types';
import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser';
import { IActiveCodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser';
import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { Position } from 'vs/editor/common/core/position';
import { IRange } from 'vs/editor/common/core/range';
import { ScrollType } from 'vs/editor/common/editorCommon';
import { IRange, Range } from 'vs/editor/common/core/range';
import { ITextModel } from 'vs/editor/common/model';
import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
@ -19,6 +17,9 @@ import * as colorRegistry from 'vs/platform/theme/common/colorRegistry';
import * as editorColorRegistry from 'vs/editor/common/core/editorColorRegistry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { interactiveEditorDiffInserted, interactiveEditorDiffRemoved, interactiveEditorRegionHighlight } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { Position } from 'vs/editor/common/core/position';
export class InteractiveEditorDiffWidget extends ZoneWidget {
@ -31,8 +32,8 @@ export class InteractiveEditorDiffWidget extends ZoneWidget {
private _dim: Dimension | undefined;
constructor(
editor: ICodeEditor,
private readonly _originalModel: ITextModel,
editor: IActiveCodeEditor,
private readonly _textModelv0: ITextModel,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
) {
@ -52,6 +53,7 @@ export class InteractiveEditorDiffWidget extends ZoneWidget {
modifiedEditor: { contributions: [] }
}, editor);
this._disposables.add(this._diffEditor);
this._diffEditor.setModel({ original: this._textModelv0, modified: editor.getModel() });
const doStyle = () => {
const theme = themeService.getColorTheme();
@ -73,60 +75,107 @@ export class InteractiveEditorDiffWidget extends ZoneWidget {
};
doStyle();
this._disposables.add(themeService.onDidColorThemeChange(doStyle));
}
protected override _fillContainer(container: HTMLElement): void {
container.appendChild(this._elements.domNode);
}
override show(range: IRange): void {
// --- show / hide --------------------
override show(): void {
throw new Error('not supported like this');
}
getEndPositionForChanges(range: Range, changes: LineRangeMapping[]): Position | undefined {
assertType(this.editor.hasModel());
const modified = this.editor.getModel();
const ranges = this._computeHiddenRanges(modified, range, changes);
if (!ranges) {
return undefined;
}
return ranges.modifiedHidden.getEndPosition();
}
showDiff(range: () => Range, changes: LineRangeMapping[]): void {
assertType(this.editor.hasModel());
this._sessionStore.clear();
this.editor.setHiddenAreas([range], InteractiveEditorDiffWidget._hideId);
this._sessionStore.add(this._diffEditor.onDidUpdateDiff(() => {
const result = this._diffEditor.getDiffComputationResult();
this._doShowForChanges(range(), result?.changes2 ?? []);
}));
this._doShowForChanges(range(), changes);
}
private _doShowForChanges(range: Range, changes: LineRangeMapping[]): void {
assertType(this.editor.hasModel());
const modified = this.editor.getModel();
const lineHeightDiff = Math.max(1, Math.abs(modified.getLineCount() - this._originalModel.getLineCount()) + (range.endLineNumber - range.startLineNumber));
const ranges = this._computeHiddenRanges(modified, range, changes);
if (!ranges) {
this.hide();
return;
}
this.editor.setHiddenAreas([ranges.modifiedHidden], InteractiveEditorDiffWidget._hideId);
this._diffEditor.getOriginalEditor().setHiddenAreas(ranges.originalDiffHidden, InteractiveEditorDiffWidget._hideId);
this._diffEditor.getModifiedEditor().setHiddenAreas(ranges.modifiedDiffHidden, InteractiveEditorDiffWidget._hideId);
const lineCountModified = ranges.modifiedHidden.endLineNumber - ranges.modifiedHidden.startLineNumber;
const lineCountOriginal = ranges.originalHidden.endLineNumber - ranges.originalHidden.startLineNumber;
const lineHeightDiff = 1 + Math.max(lineCountModified, lineCountOriginal);
const lineHeightPadding = (this.editor.getOption(EditorOption.lineHeight) / 12) /* padding-top/bottom*/;
this._diffEditor.setModel({ original: this._originalModel, modified });
this._diffEditor.revealRange(range, ScrollType.Immediate);
this._diffEditor.getModifiedEditor().setHiddenAreas(invertRange(range, modified), InteractiveEditorDiffWidget._hideId);
const position = ranges.modifiedHidden.getEndPosition();
super.show(position, lineHeightDiff + lineHeightPadding);
}
const updateHiddenAreasOriginal = () => {
// todo@jrieken this needs work when both are equal
const changes = this._diffEditor.getLineChanges();
if (!changes) {
return;
}
let startLine = Number.MAX_VALUE;
let endLine = 0;
for (const change of changes) {
startLine = Math.min(startLine, change.originalStartLineNumber, change.modifiedStartLineNumber);
endLine = Math.max(endLine, change.originalEndLineNumber || change.originalStartLineNumber, change.modifiedEndLineNumber || change.modifiedStartLineNumber);
}
const combinedRange = this._originalModel.validateRange({ startLineNumber: startLine, startColumn: 1, endLineNumber: endLine, endColumn: Number.MAX_VALUE });
private _computeHiddenRanges(model: ITextModel, range: Range, changes: LineRangeMapping[]) {
if (changes.length === 0) {
return undefined;
}
const hiddenRangesOriginal = invertRange(combinedRange, this._originalModel);
this._diffEditor.getOriginalEditor().setHiddenAreas(hiddenRangesOriginal, InteractiveEditorDiffWidget._hideId);
let originalLineRange = changes[0].originalRange;
let modifiedLineRange = changes[0].modifiedRange;
for (let i = 1; i < changes.length; i++) {
originalLineRange = originalLineRange.join(changes[i].originalRange);
modifiedLineRange = modifiedLineRange.join(changes[i].modifiedRange);
}
// const hiddenRangesModified = invertRange(combinedRange, modified);
// this._diffEditor.getModifiedEditor().setHiddenAreas(hiddenRangesModified, InteractiveEditorDiffWidget._hideId);
};
this._diffEditor.onDidUpdateDiff(updateHiddenAreasOriginal, undefined, this._sessionStore);
updateHiddenAreasOriginal();
const startDelta = modifiedLineRange.startLineNumber - range.startLineNumber;
if (startDelta > 0) {
modifiedLineRange = new LineRange(modifiedLineRange.startLineNumber - startDelta, modifiedLineRange.endLineNumberExclusive);
originalLineRange = new LineRange(originalLineRange.startLineNumber - startDelta, originalLineRange.endLineNumberExclusive);
}
super.show(new Position(range.endLineNumber, range.endColumn), lineHeightDiff + lineHeightPadding);
const endDelta = range.endLineNumber - (modifiedLineRange.endLineNumberExclusive - 1);
if (endDelta > 0) {
modifiedLineRange = new LineRange(modifiedLineRange.startLineNumber, modifiedLineRange.endLineNumberExclusive + endDelta);
originalLineRange = new LineRange(originalLineRange.startLineNumber, originalLineRange.endLineNumberExclusive + endDelta);
}
const originalHidden = asRange(originalLineRange, this._textModelv0);
const originalDiffHidden = invertRange(originalHidden, this._textModelv0);
const modifiedHidden = asRange(modifiedLineRange, model);
const modifiedDiffHidden = invertRange(modifiedHidden, model);
return { originalHidden, originalDiffHidden, modifiedHidden, modifiedDiffHidden };
}
override hide(): void {
this.editor.setHiddenAreas([], InteractiveEditorDiffWidget._hideId);
this._diffEditor.getOriginalEditor().setHiddenAreas([], InteractiveEditorDiffWidget._hideId);
this._diffEditor.getModifiedEditor().setHiddenAreas([], InteractiveEditorDiffWidget._hideId);
this._diffEditor.setModel(null);
super.hide();
}
// --- layout -------------------------
protected override _onWidth(widthInPixel: number): void {
if (this._dim) {
this._doLayout(this._dim.height, widthInPixel);
@ -143,13 +192,29 @@ export class InteractiveEditorDiffWidget extends ZoneWidget {
}
}
function invertRange(range: IRange, model: ITextModel): IRange[] {
const result: IRange[] = [];
if (range.startLineNumber > 1) {
result.push({ startLineNumber: 1, startColumn: 1, endLineNumber: range.startLineNumber - 1, endColumn: 1 });
}
if (range.endLineNumber < model.getLineCount()) {
result.push({ startLineNumber: range.endLineNumber + 1, startColumn: 1, endLineNumber: model.getLineCount(), endColumn: 1 });
function invertRange(range: IRange, model: ITextModel): Range[] {
const result: Range[] = [];
if (Range.isEmpty(range)) {
//result.push(model.getFullModelRange());
// todo@jrieken
// cannot hide everything, return [] instead
} else {
if (range.startLineNumber > 1) {
result.push(new Range(1, 1, range.startLineNumber - 1, 1));
}
if (range.endLineNumber < model.getLineCount()) {
result.push(new Range(range.endLineNumber + 1, 1, model.getLineCount(), 1));
}
}
return result;
}
function asRange(lineRange: LineRange, model: ITextModel): Range {
if (lineRange.isEmpty) {
return new Range(lineRange.startLineNumber, 1, lineRange.startLineNumber, 1);
} else {
const endLine = lineRange.endLineNumberExclusive - 1;
return new Range(lineRange.startLineNumber, 1, endLine, model.getLineMaxColumn(endLine));
}
}