mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-29 04:53:33 +01:00
442 lines
16 KiB
TypeScript
442 lines
16 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 { Dimension, h } from 'vs/base/browser/dom';
|
|
import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
|
|
import { assertType } from 'vs/base/common/types';
|
|
import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser';
|
|
import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
|
|
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
|
import { Range } from 'vs/editor/common/core/range';
|
|
import { IModelDecorationOptions, IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model';
|
|
import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget';
|
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
|
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 { INLINE_CHAT_ID, inlineChatDiffInserted, inlineChatDiffRemoved, inlineChatRegionHighlight } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
|
|
import { LineRange } from 'vs/editor/common/core/lineRange';
|
|
import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
|
|
import { Position } from 'vs/editor/common/core/position';
|
|
import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';
|
|
import { IEditorDecorationsCollection, ScrollType } from 'vs/editor/common/editorCommon';
|
|
import { ILogService } from 'vs/platform/log/common/log';
|
|
import { lineRangeAsRange, invertLineRange } from 'vs/workbench/contrib/inlineChat/browser/utils';
|
|
import { ResourceLabel } from 'vs/workbench/browser/labels';
|
|
import { URI } from 'vs/base/common/uri';
|
|
import { TextEdit } from 'vs/editor/common/languages';
|
|
import { FileKind } from 'vs/platform/files/common/files';
|
|
import { IModelService } from 'vs/editor/common/services/model';
|
|
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
|
import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
|
|
|
|
export class InlineChatLivePreviewWidget extends ZoneWidget {
|
|
|
|
private static readonly _hideId = 'overlayDiff';
|
|
|
|
private readonly _elements = h('div.inline-chat-diff-widget@domNode');
|
|
|
|
private readonly _sessionStore = this._disposables.add(new DisposableStore());
|
|
private readonly _diffEditor: IDiffEditor;
|
|
private readonly _inlineDiffDecorations: IEditorDecorationsCollection;
|
|
private _dim: Dimension | undefined;
|
|
private _isVisible: boolean = false;
|
|
|
|
constructor(
|
|
editor: ICodeEditor,
|
|
private readonly _session: Session,
|
|
@IInstantiationService instantiationService: IInstantiationService,
|
|
@IThemeService themeService: IThemeService,
|
|
@ILogService private readonly _logService: ILogService,
|
|
) {
|
|
super(editor, { showArrow: false, showFrame: false, isResizeable: false, isAccessible: true, allowUnlimitedHeight: true, showInHiddenAreas: true, ordinal: 10000 + 1 });
|
|
super.create();
|
|
assertType(editor.hasModel());
|
|
|
|
this._inlineDiffDecorations = editor.createDecorationsCollection();
|
|
|
|
const diffContributions = EditorExtensionsRegistry
|
|
.getEditorContributions()
|
|
.filter(c => c.id !== INLINE_CHAT_ID);
|
|
|
|
this._diffEditor = instantiationService.createInstance(EmbeddedDiffEditorWidget, this._elements.domNode, {
|
|
scrollbar: { useShadows: false, alwaysConsumeMouseWheel: false },
|
|
scrollBeyondLastLine: false,
|
|
renderMarginRevertIcon: true,
|
|
renderOverviewRuler: false,
|
|
rulers: undefined,
|
|
overviewRulerBorder: undefined,
|
|
overviewRulerLanes: 0,
|
|
diffAlgorithm: 'advanced',
|
|
splitViewDefaultRatio: 0.35,
|
|
padding: { top: 0, bottom: 0 },
|
|
folding: false,
|
|
diffCodeLens: false,
|
|
stickyScroll: { enabled: false },
|
|
minimap: { enabled: false },
|
|
isInEmbeddedEditor: true,
|
|
overflowWidgetsDomNode: editor.getOverflowWidgetsDomNode()
|
|
}, {
|
|
originalEditor: { contributions: diffContributions },
|
|
modifiedEditor: { contributions: diffContributions }
|
|
}, editor);
|
|
this._disposables.add(this._diffEditor);
|
|
this._diffEditor.setModel({ original: this._session.textModel0, modified: editor.getModel() });
|
|
|
|
const doStyle = () => {
|
|
const theme = themeService.getColorTheme();
|
|
const overrides: [target: string, source: string][] = [
|
|
[colorRegistry.editorBackground, inlineChatRegionHighlight],
|
|
[editorColorRegistry.editorGutter, inlineChatRegionHighlight],
|
|
[colorRegistry.diffInsertedLine, inlineChatDiffInserted],
|
|
[colorRegistry.diffInserted, inlineChatDiffInserted],
|
|
[colorRegistry.diffRemovedLine, inlineChatDiffRemoved],
|
|
[colorRegistry.diffRemoved, inlineChatDiffRemoved],
|
|
];
|
|
|
|
for (const [target, source] of overrides) {
|
|
const value = theme.getColor(source);
|
|
if (value) {
|
|
this._elements.domNode.style.setProperty(colorRegistry.asCssVariableName(target), String(value));
|
|
}
|
|
}
|
|
};
|
|
doStyle();
|
|
this._disposables.add(themeService.onDidColorThemeChange(doStyle));
|
|
}
|
|
|
|
override dispose(): void {
|
|
this._inlineDiffDecorations.clear();
|
|
super.dispose();
|
|
}
|
|
|
|
protected override _fillContainer(container: HTMLElement): void {
|
|
container.appendChild(this._elements.domNode);
|
|
}
|
|
|
|
// --- show / hide --------------------
|
|
|
|
get isVisible(): boolean {
|
|
return this._isVisible;
|
|
}
|
|
|
|
override hide(): void {
|
|
this._cleanupFullDiff();
|
|
this._cleanupInlineDiff();
|
|
this._sessionStore.clear();
|
|
super.hide();
|
|
this._isVisible = false;
|
|
}
|
|
|
|
override show(): void {
|
|
assertType(this.editor.hasModel());
|
|
this._sessionStore.clear();
|
|
|
|
this._sessionStore.add(this._diffEditor.onDidUpdateDiff(() => {
|
|
const result = this._diffEditor.getDiffComputationResult();
|
|
const hasFocus = this._diffEditor.hasTextFocus();
|
|
this._updateFromChanges(this._session.wholeRange.value, result?.changes2 ?? []);
|
|
// TODO@jrieken find a better fix for this. this is the challenge:
|
|
// the _doShowForChanges method invokes show of the zone widget which removes and adds the
|
|
// zone and overlay parts. this dettaches and reattaches the dom nodes which means they lose
|
|
// focus
|
|
if (hasFocus) {
|
|
this._diffEditor.focus();
|
|
}
|
|
}));
|
|
this._updateFromChanges(this._session.wholeRange.value, this._session.lastTextModelChanges);
|
|
this._isVisible = true;
|
|
}
|
|
|
|
private _updateFromChanges(range: Range, changes: readonly LineRangeMapping[]): void {
|
|
assertType(this.editor.hasModel());
|
|
|
|
if (changes.length === 0 || this._session.textModel0.getValueLength() === 0) {
|
|
// no change or changes to an empty file
|
|
this._logService.debug('[IE] livePreview-mode: no diff');
|
|
this.hide();
|
|
|
|
} else if (changes.every(isInlineDiffFriendly)) {
|
|
// simple changes
|
|
this._logService.debug('[IE] livePreview-mode: inline diff');
|
|
this._cleanupFullDiff();
|
|
this._renderChangesWithInlineDiff(changes);
|
|
|
|
} else {
|
|
// complex changes
|
|
this._logService.debug('[IE] livePreview-mode: full diff');
|
|
this._cleanupInlineDiff();
|
|
this._renderChangesWithFullDiff(changes, range);
|
|
}
|
|
}
|
|
|
|
// --- inline diff
|
|
|
|
private _renderChangesWithInlineDiff(changes: readonly LineRangeMapping[]) {
|
|
const original = this._session.textModel0;
|
|
|
|
const decorations: IModelDeltaDecoration[] = [];
|
|
|
|
for (const { innerChanges } of changes) {
|
|
if (!innerChanges) {
|
|
continue;
|
|
}
|
|
for (const { modifiedRange, originalRange } of innerChanges) {
|
|
|
|
const options: IModelDecorationOptions = {
|
|
description: 'interactive-diff-inline',
|
|
showIfCollapsed: true,
|
|
};
|
|
|
|
if (!modifiedRange.isEmpty()) {
|
|
options.className = 'inline-chat-lines-inserted-range';
|
|
}
|
|
|
|
if (!originalRange.isEmpty()) {
|
|
let content = original.getValueInRange(originalRange);
|
|
if (content.length > 7) {
|
|
content = content.substring(0, 7) + '…';
|
|
}
|
|
options.before = {
|
|
content,
|
|
inlineClassName: 'inline-chat-lines-deleted-range-inline'
|
|
};
|
|
}
|
|
|
|
decorations.push({
|
|
range: modifiedRange,
|
|
options
|
|
});
|
|
}
|
|
}
|
|
|
|
this._inlineDiffDecorations.set(decorations);
|
|
}
|
|
|
|
private _cleanupInlineDiff() {
|
|
this._inlineDiffDecorations.clear();
|
|
}
|
|
|
|
// --- full diff
|
|
|
|
private _renderChangesWithFullDiff(changes: readonly LineRangeMapping[], range: Range) {
|
|
|
|
const modified = this.editor.getModel()!;
|
|
const ranges = this._computeHiddenRanges(modified, range, changes);
|
|
|
|
this._hideEditorRanges(this.editor, [ranges.modifiedHidden]);
|
|
this._hideEditorRanges(this._diffEditor.getOriginalEditor(), ranges.originalDiffHidden);
|
|
this._hideEditorRanges(this._diffEditor.getModifiedEditor(), ranges.modifiedDiffHidden);
|
|
|
|
this._diffEditor.revealLine(ranges.modifiedHidden.startLineNumber, ScrollType.Immediate);
|
|
|
|
const lineCountModified = ranges.modifiedHidden.length;
|
|
const lineCountOriginal = ranges.originalHidden.length;
|
|
|
|
const lineHeightDiff = Math.max(lineCountModified, lineCountOriginal);
|
|
const lineHeightPadding = (this.editor.getOption(EditorOption.lineHeight) / 12) /* padding-top/bottom*/;
|
|
const heightInLines = lineHeightDiff + lineHeightPadding;
|
|
|
|
super.show(ranges.anchor, heightInLines);
|
|
this._logService.debug(`[IE] diff SHOWING at ${ranges.anchor} with ${heightInLines} lines height`);
|
|
}
|
|
|
|
private _cleanupFullDiff() {
|
|
this.editor.setHiddenAreas([], InlineChatLivePreviewWidget._hideId);
|
|
this._diffEditor.getOriginalEditor().setHiddenAreas([], InlineChatLivePreviewWidget._hideId);
|
|
this._diffEditor.getModifiedEditor().setHiddenAreas([], InlineChatLivePreviewWidget._hideId);
|
|
super.hide();
|
|
}
|
|
|
|
private _computeHiddenRanges(model: ITextModel, range: Range, changes: readonly LineRangeMapping[]) {
|
|
assertType(changes.length > 0);
|
|
|
|
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 startDelta = modifiedLineRange.startLineNumber - range.startLineNumber;
|
|
if (startDelta > 0) {
|
|
modifiedLineRange = new LineRange(modifiedLineRange.startLineNumber - startDelta, modifiedLineRange.endLineNumberExclusive);
|
|
originalLineRange = new LineRange(originalLineRange.startLineNumber - startDelta, originalLineRange.endLineNumberExclusive);
|
|
}
|
|
|
|
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 originalDiffHidden = invertLineRange(originalLineRange, this._session.textModel0);
|
|
const modifiedDiffHidden = invertLineRange(modifiedLineRange, model);
|
|
|
|
return {
|
|
originalHidden: originalLineRange,
|
|
originalDiffHidden,
|
|
modifiedHidden: modifiedLineRange,
|
|
modifiedDiffHidden,
|
|
anchor: new Position(modifiedLineRange.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER)
|
|
};
|
|
}
|
|
|
|
private _hideEditorRanges(editor: ICodeEditor, lineRanges: LineRange[]): void {
|
|
lineRanges = lineRanges.filter(range => !range.isEmpty);
|
|
if (lineRanges.length === 0) {
|
|
// todo?
|
|
this._logService.debug(`[IE] diff NOTHING to hide for ${editor.getId()} with ${String(editor.getModel()?.uri)}`);
|
|
return;
|
|
}
|
|
const ranges = lineRanges.map(lineRangeAsRange);
|
|
editor.setHiddenAreas(ranges, InlineChatLivePreviewWidget._hideId);
|
|
this._logService.debug(`[IE] diff HIDING ${ranges} for ${editor.getId()} with ${String(editor.getModel()?.uri)}`);
|
|
}
|
|
|
|
protected override revealRange(range: Range, isLastLine: boolean): void {
|
|
// ignore
|
|
}
|
|
|
|
// --- layout -------------------------
|
|
|
|
protected override _onWidth(widthInPixel: number): void {
|
|
if (this._dim) {
|
|
this._doLayout(this._dim.height, widthInPixel);
|
|
}
|
|
}
|
|
|
|
protected override _doLayout(heightInPixel: number, widthInPixel: number): void {
|
|
const newDim = new Dimension(widthInPixel, heightInPixel);
|
|
if (!Dimension.equals(this._dim, newDim)) {
|
|
this._dim = newDim;
|
|
this._diffEditor.layout(this._dim.with(undefined, this._dim.height - 12 /* padding */));
|
|
this._logService.debug('[IE] diff LAYOUT', this._dim);
|
|
}
|
|
}
|
|
}
|
|
|
|
function isInlineDiffFriendly(mapping: LineRangeMapping): boolean {
|
|
if (!mapping.modifiedRange.equals(mapping.originalRange)) {
|
|
return false;
|
|
}
|
|
if (!mapping.innerChanges) {
|
|
return false;
|
|
}
|
|
for (const { modifiedRange, originalRange } of mapping.innerChanges) {
|
|
if (Range.spansMultipleLines(modifiedRange) || Range.spansMultipleLines(originalRange)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
export class InlineChatFileCreatePreviewWidget extends ZoneWidget {
|
|
|
|
private readonly _elements = h('div.inline-chat-newfile-widget@domNode', [
|
|
h('div.title.show-file-icons@title'),
|
|
h('div.editor@editor'),
|
|
]);
|
|
|
|
private readonly _title: ResourceLabel;
|
|
private readonly _previewEditor: ICodeEditor;
|
|
private readonly _previewModel = new MutableDisposable();
|
|
private _dim: Dimension | undefined;
|
|
|
|
constructor(
|
|
parentEditor: ICodeEditor,
|
|
@IInstantiationService instaService: IInstantiationService,
|
|
@IModelService private readonly _modelService: IModelService,
|
|
@IThemeService themeService: IThemeService,
|
|
|
|
) {
|
|
super(parentEditor, { showArrow: false, showFrame: false, isResizeable: false, isAccessible: true, showInHiddenAreas: true, ordinal: 10000 + 2 });
|
|
super.create();
|
|
|
|
this._title = instaService.createInstance(ResourceLabel, this._elements.title, { supportIcons: true });
|
|
this._previewEditor = instaService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, {
|
|
scrollBeyondLastLine: false,
|
|
stickyScroll: { enabled: false },
|
|
readOnly: true,
|
|
minimap: { enabled: false },
|
|
scrollbar: { alwaysConsumeMouseWheel: false },
|
|
}, { isSimpleWidget: true, contributions: [] }, parentEditor);
|
|
|
|
const doStyle = () => {
|
|
const theme = themeService.getColorTheme();
|
|
const overrides: [target: string, source: string][] = [
|
|
[colorRegistry.editorBackground, inlineChatRegionHighlight],
|
|
[editorColorRegistry.editorGutter, inlineChatRegionHighlight],
|
|
];
|
|
|
|
for (const [target, source] of overrides) {
|
|
const value = theme.getColor(source);
|
|
if (value) {
|
|
this._elements.domNode.style.setProperty(colorRegistry.asCssVariableName(target), String(value));
|
|
}
|
|
}
|
|
};
|
|
doStyle();
|
|
this._disposables.add(themeService.onDidColorThemeChange(doStyle));
|
|
}
|
|
|
|
override dispose(): void {
|
|
this._title.dispose();
|
|
this._previewEditor.dispose();
|
|
this._previewModel.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
protected override _fillContainer(container: HTMLElement): void {
|
|
container.appendChild(this._elements.domNode);
|
|
}
|
|
|
|
override show(): void {
|
|
throw new Error('Use showFileCreation');
|
|
}
|
|
|
|
showCreation(where: Range, uri: URI, edits: TextEdit[]): void {
|
|
|
|
this._title.element.setFile(uri, { fileKind: FileKind.FILE });
|
|
|
|
const model = this._modelService.createModel('', null, undefined, true);
|
|
model.applyEdits(edits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)));
|
|
this._previewModel.value = model;
|
|
this._previewEditor.setModel(model);
|
|
|
|
const lines = Math.min(7, model.getLineCount());
|
|
const lineHeightPadding = (this.editor.getOption(EditorOption.lineHeight) / 12) /* padding-top/bottom*/;
|
|
|
|
|
|
super.show(where, lines + 1 + lineHeightPadding);
|
|
}
|
|
|
|
// --- layout
|
|
|
|
protected override revealRange(range: Range, isLastLine: boolean): void {
|
|
// ignore
|
|
}
|
|
|
|
protected override _onWidth(widthInPixel: number): void {
|
|
if (this._dim) {
|
|
this._doLayout(this._dim.height, widthInPixel);
|
|
}
|
|
}
|
|
|
|
protected override _doLayout(heightInPixel: number, widthInPixel: number): void {
|
|
|
|
const { lineNumbersLeft } = this.editor.getLayoutInfo();
|
|
this._elements.title.style.marginLeft = `${lineNumbersLeft}px`;
|
|
|
|
const newDim = new Dimension(widthInPixel, heightInPixel);
|
|
if (!Dimension.equals(this._dim, newDim)) {
|
|
this._dim = newDim;
|
|
const oneLineHeightInPx = this.editor.getOption(EditorOption.lineHeight);
|
|
this._previewEditor.layout(this._dim.with(undefined, this._dim.height - oneLineHeightInPx /* title */));
|
|
}
|
|
}
|
|
}
|