in patched-vscode/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts [60:439]
private readonly _canIgnoreViewZoneUpdateEvent: () => boolean,
private readonly _origViewZonesToIgnore: Set<string>,
private readonly _modViewZonesToIgnore: Set<string>,
@IClipboardService private readonly _clipboardService: IClipboardService,
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
) {
super();
const state = observableValue('invalidateAlignmentsState', 0);
const updateImmediately = this._register(new RunOnceScheduler(() => {
state.set(state.get() + 1, undefined);
}, 0));
this._register(this._editors.original.onDidChangeViewZones((_args) => { if (!this._canIgnoreViewZoneUpdateEvent()) { updateImmediately.schedule(); } }));
this._register(this._editors.modified.onDidChangeViewZones((_args) => { if (!this._canIgnoreViewZoneUpdateEvent()) { updateImmediately.schedule(); } }));
this._register(this._editors.original.onDidChangeConfiguration((args) => {
if (args.hasChanged(EditorOption.wrappingInfo) || args.hasChanged(EditorOption.lineHeight)) { updateImmediately.schedule(); }
}));
this._register(this._editors.modified.onDidChangeConfiguration((args) => {
if (args.hasChanged(EditorOption.wrappingInfo) || args.hasChanged(EditorOption.lineHeight)) { updateImmediately.schedule(); }
}));
const originalModelTokenizationCompleted = this._diffModel.map(m =>
m ? observableFromEvent(m.model.original.onDidChangeTokens, () => m.model.original.tokenization.backgroundTokenizationState === BackgroundTokenizationState.Completed) : undefined
).map((m, reader) => m?.read(reader));
const alignments = derived<ILineRangeAlignment[] | null>((reader) => {
/** @description alignments */
const diffModel = this._diffModel.read(reader);
const diff = diffModel?.diff.read(reader);
if (!diffModel || !diff) { return null; }
state.read(reader);
const renderSideBySide = this._options.renderSideBySide.read(reader);
const innerHunkAlignment = renderSideBySide;
return computeRangeAlignment(
this._editors.original,
this._editors.modified,
diff.mappings,
this._origViewZonesToIgnore,
this._modViewZonesToIgnore,
innerHunkAlignment
);
});
const alignmentsSyncedMovedText = derived<ILineRangeAlignment[] | null>((reader) => {
/** @description alignmentsSyncedMovedText */
const syncedMovedText = this._diffModel.read(reader)?.movedTextToCompare.read(reader);
if (!syncedMovedText) { return null; }
state.read(reader);
const mappings = syncedMovedText.changes.map(c => new DiffMapping(c));
// TODO dont include alignments outside syncedMovedText
return computeRangeAlignment(
this._editors.original,
this._editors.modified,
mappings,
this._origViewZonesToIgnore,
this._modViewZonesToIgnore,
true
);
});
function createFakeLinesDiv(): HTMLElement {
const r = document.createElement('div');
r.className = 'diagonal-fill';
return r;
}
const alignmentViewZonesDisposables = this._register(new DisposableStore());
this.viewZones = derivedWithStore<{ orig: IObservableViewZone[]; mod: IObservableViewZone[] }>(this, (reader, store) => {
alignmentViewZonesDisposables.clear();
const alignmentsVal = alignments.read(reader) || [];
const origViewZones: IObservableViewZone[] = [];
const modViewZones: IObservableViewZone[] = [];
const modifiedTopPaddingVal = this._modifiedTopPadding.read(reader);
if (modifiedTopPaddingVal > 0) {
modViewZones.push({
afterLineNumber: 0,
domNode: document.createElement('div'),
heightInPx: modifiedTopPaddingVal,
showInHiddenAreas: true,
suppressMouseDown: true,
});
}
const originalTopPaddingVal = this._originalTopPadding.read(reader);
if (originalTopPaddingVal > 0) {
origViewZones.push({
afterLineNumber: 0,
domNode: document.createElement('div'),
heightInPx: originalTopPaddingVal,
showInHiddenAreas: true,
suppressMouseDown: true,
});
}
const renderSideBySide = this._options.renderSideBySide.read(reader);
const deletedCodeLineBreaksComputer = !renderSideBySide ? this._editors.modified._getViewModel()?.createLineBreaksComputer() : undefined;
if (deletedCodeLineBreaksComputer) {
const originalModel = this._editors.original.getModel()!;
for (const a of alignmentsVal) {
if (a.diff) {
for (let i = a.originalRange.startLineNumber; i < a.originalRange.endLineNumberExclusive; i++) {
// `i` can be out of bound when the diff has not been updated yet.
// In this case, we do an early return.
// TODO@hediet: Fix this by applying the edit directly to the diff model, so that the diff is always valid.
if (i > originalModel.getLineCount()) {
return { orig: origViewZones, mod: modViewZones };
}
deletedCodeLineBreaksComputer?.addRequest(originalModel.getLineContent(i), null, null);
}
}
}
}
const lineBreakData = deletedCodeLineBreaksComputer?.finalize() ?? [];
let lineBreakDataIdx = 0;
const modLineHeight = this._editors.modified.getOption(EditorOption.lineHeight);
const syncedMovedText = this._diffModel.read(reader)?.movedTextToCompare.read(reader);
const mightContainNonBasicASCII = this._editors.original.getModel()?.mightContainNonBasicASCII() ?? false;
const mightContainRTL = this._editors.original.getModel()?.mightContainRTL() ?? false;
const renderOptions = RenderOptions.fromEditor(this._editors.modified);
for (const a of alignmentsVal) {
if (a.diff && !renderSideBySide) {
if (!a.originalRange.isEmpty) {
originalModelTokenizationCompleted.read(reader); // Update view-zones once tokenization completes
const deletedCodeDomNode = document.createElement('div');
deletedCodeDomNode.classList.add('view-lines', 'line-delete', 'monaco-mouse-cursor-text');
const originalModel = this._editors.original.getModel()!;
// `a.originalRange` can be out of bound when the diff has not been updated yet.
// In this case, we do an early return.
// TODO@hediet: Fix this by applying the edit directly to the diff model, so that the diff is always valid.
if (a.originalRange.endLineNumberExclusive - 1 > originalModel.getLineCount()) {
return { orig: origViewZones, mod: modViewZones };
}
const source = new LineSource(
a.originalRange.mapToLineArray(l => originalModel.tokenization.getLineTokens(l)),
a.originalRange.mapToLineArray(_ => lineBreakData[lineBreakDataIdx++]),
mightContainNonBasicASCII,
mightContainRTL,
);
const decorations: InlineDecoration[] = [];
for (const i of a.diff.innerChanges || []) {
decorations.push(new InlineDecoration(
i.originalRange.delta(-(a.diff.original.startLineNumber - 1)),
diffDeleteDecoration.className!,
InlineDecorationType.Regular
));
}
const result = renderLines(source, renderOptions, decorations, deletedCodeDomNode);
const marginDomNode = document.createElement('div');
marginDomNode.className = 'inline-deleted-margin-view-zone';
applyFontInfo(marginDomNode, renderOptions.fontInfo);
if (this._options.renderIndicators.read(reader)) {
for (let i = 0; i < result.heightInLines; i++) {
const marginElement = document.createElement('div');
marginElement.className = `delete-sign ${ThemeIcon.asClassName(diffRemoveIcon)}`;
marginElement.setAttribute('style', `position:absolute;top:${i * modLineHeight}px;width:${renderOptions.lineDecorationsWidth}px;height:${modLineHeight}px;right:0;`);
marginDomNode.appendChild(marginElement);
}
}
let zoneId: string | undefined = undefined;
alignmentViewZonesDisposables.add(
new InlineDiffDeletedCodeMargin(
() => assertIsDefined(zoneId),
marginDomNode,
this._editors.modified,
a.diff,
this._diffEditorWidget,
result.viewLineCounts,
this._editors.original.getModel()!,
this._contextMenuService,
this._clipboardService,
)
);
for (let i = 0; i < result.viewLineCounts.length; i++) {
const count = result.viewLineCounts[i];
// Account for wrapped lines in the (collapsed) original editor (which doesn't wrap lines).
if (count > 1) {
origViewZones.push({
afterLineNumber: a.originalRange.startLineNumber + i,
domNode: createFakeLinesDiv(),
heightInPx: (count - 1) * modLineHeight,
showInHiddenAreas: true,
suppressMouseDown: true,
});
}
}
modViewZones.push({
afterLineNumber: a.modifiedRange.startLineNumber - 1,
domNode: deletedCodeDomNode,
heightInPx: result.heightInLines * modLineHeight,
minWidthInPx: result.minWidthInPx,
marginDomNode,
setZoneId(id) { zoneId = id; },
showInHiddenAreas: true,
suppressMouseDown: true,
});
}
const marginDomNode = document.createElement('div');
marginDomNode.className = 'gutter-delete';
origViewZones.push({
afterLineNumber: a.originalRange.endLineNumberExclusive - 1,
domNode: createFakeLinesDiv(),
heightInPx: a.modifiedHeightInPx,
marginDomNode,
showInHiddenAreas: true,
suppressMouseDown: true,
});
} else {
const delta = a.modifiedHeightInPx - a.originalHeightInPx;
if (delta > 0) {
if (syncedMovedText?.lineRangeMapping.original.delta(-1).deltaLength(2).contains(a.originalRange.endLineNumberExclusive - 1)) {
continue;
}
origViewZones.push({
afterLineNumber: a.originalRange.endLineNumberExclusive - 1,
domNode: createFakeLinesDiv(),
heightInPx: delta,
showInHiddenAreas: true,
suppressMouseDown: true,
});
} else {
if (syncedMovedText?.lineRangeMapping.modified.delta(-1).deltaLength(2).contains(a.modifiedRange.endLineNumberExclusive - 1)) {
continue;
}
function createViewZoneMarginArrow(): HTMLElement {
const arrow = document.createElement('div');
arrow.className = 'arrow-revert-change ' + ThemeIcon.asClassName(Codicon.arrowRight);
store.add(addDisposableListener(arrow, 'mousedown', e => e.stopPropagation()));
store.add(addDisposableListener(arrow, 'click', e => {
e.stopPropagation();
_diffEditorWidget.revert(a.diff!);
}));
return $('div', {}, arrow);
}
let marginDomNode: HTMLElement | undefined = undefined;
if (a.diff && a.diff.modified.isEmpty && this._options.shouldRenderOldRevertArrows.read(reader)) {
marginDomNode = createViewZoneMarginArrow();
}
modViewZones.push({
afterLineNumber: a.modifiedRange.endLineNumberExclusive - 1,
domNode: createFakeLinesDiv(),
heightInPx: -delta,
marginDomNode,
showInHiddenAreas: true,
suppressMouseDown: true,
});
}
}
}
for (const a of alignmentsSyncedMovedText.read(reader) ?? []) {
if (!syncedMovedText?.lineRangeMapping.original.intersect(a.originalRange)
|| !syncedMovedText?.lineRangeMapping.modified.intersect(a.modifiedRange)) {
// ignore unrelated alignments outside the synced moved text
continue;
}
const delta = a.modifiedHeightInPx - a.originalHeightInPx;
if (delta > 0) {
origViewZones.push({
afterLineNumber: a.originalRange.endLineNumberExclusive - 1,
domNode: createFakeLinesDiv(),
heightInPx: delta,
showInHiddenAreas: true,
suppressMouseDown: true,
});
} else {
modViewZones.push({
afterLineNumber: a.modifiedRange.endLineNumberExclusive - 1,
domNode: createFakeLinesDiv(),
heightInPx: -delta,
showInHiddenAreas: true,
suppressMouseDown: true,
});
}
}
return { orig: origViewZones, mod: modViewZones };
});
let ignoreChange = false;
this._register(this._editors.original.onDidScrollChange(e => {
if (e.scrollLeftChanged && !ignoreChange) {
ignoreChange = true;
this._editors.modified.setScrollLeft(e.scrollLeft);
ignoreChange = false;
}
}));
this._register(this._editors.modified.onDidScrollChange(e => {
if (e.scrollLeftChanged && !ignoreChange) {
ignoreChange = true;
this._editors.original.setScrollLeft(e.scrollLeft);
ignoreChange = false;
}
}));
this._originalScrollTop = observableFromEvent(this._editors.original.onDidScrollChange, () => /** @description original.getScrollTop */ this._editors.original.getScrollTop());
this._modifiedScrollTop = observableFromEvent(this._editors.modified.onDidScrollChange, () => /** @description modified.getScrollTop */ this._editors.modified.getScrollTop());
// origExtraHeight + origOffset - origScrollTop = modExtraHeight + modOffset - modScrollTop
// origScrollTop = origExtraHeight + origOffset - modExtraHeight - modOffset + modScrollTop
// modScrollTop = modExtraHeight + modOffset - origExtraHeight - origOffset + origScrollTop
// origOffset - modOffset = heightOfLines(1..Y) - heightOfLines(1..X)
// origScrollTop >= 0, modScrollTop >= 0
this._register(autorun(reader => {
/** @description update scroll modified */
const newScrollTopModified = this._originalScrollTop.read(reader)
- (this._originalScrollOffsetAnimated.get() - this._modifiedScrollOffsetAnimated.read(reader))
- (this._originalTopPadding.get() - this._modifiedTopPadding.read(reader));
if (newScrollTopModified !== this._editors.modified.getScrollTop()) {
this._editors.modified.setScrollTop(newScrollTopModified, ScrollType.Immediate);
}
}));
this._register(autorun(reader => {
/** @description update scroll original */
const newScrollTopOriginal = this._modifiedScrollTop.read(reader)
- (this._modifiedScrollOffsetAnimated.get() - this._originalScrollOffsetAnimated.read(reader))
- (this._modifiedTopPadding.get() - this._originalTopPadding.read(reader));
if (newScrollTopOriginal !== this._editors.original.getScrollTop()) {
this._editors.original.setScrollTop(newScrollTopOriginal, ScrollType.Immediate);
}
}));
this._register(autorun(reader => {
/** @description update editor top offsets */
const m = this._diffModel.read(reader)?.movedTextToCompare.read(reader);
let deltaOrigToMod = 0;
if (m) {
const trueTopOriginal = this._editors.original.getTopForLineNumber(m.lineRangeMapping.original.startLineNumber, true) - this._originalTopPadding.get();
const trueTopModified = this._editors.modified.getTopForLineNumber(m.lineRangeMapping.modified.startLineNumber, true) - this._modifiedTopPadding.get();
deltaOrigToMod = trueTopModified - trueTopOriginal;
}
if (deltaOrigToMod > 0) {
this._modifiedTopPadding.set(0, undefined);
this._originalTopPadding.set(deltaOrigToMod, undefined);
} else if (deltaOrigToMod < 0) {
this._modifiedTopPadding.set(-deltaOrigToMod, undefined);
this._originalTopPadding.set(0, undefined);
} else {
setTimeout(() => {
this._modifiedTopPadding.set(0, undefined);
this._originalTopPadding.set(0, undefined);
}, 400);
}
if (this._editors.modified.hasTextFocus()) {
this._originalScrollOffset.set(this._modifiedScrollOffset.get() - deltaOrigToMod, undefined, true);
} else {
this._modifiedScrollOffset.set(this._originalScrollOffset.get() + deltaOrigToMod, undefined, true);
}
}));
}