UNPKG

monaco-editor

Version:
269 lines (268 loc) • 16.4 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { h } from '../../../../../base/browser/dom.js'; import { ActionBar } from '../../../../../base/browser/ui/actionbar/actionbar.js'; import { Action } from '../../../../../base/common/actions.js'; import { booleanComparator, compareBy, numberComparator, tieBreakComparators } from '../../../../../base/common/arrays.js'; import { findMaxIdxBy } from '../../../../../base/common/arraysFind.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { autorun, autorunHandleChanges, autorunWithStore, constObservable, derived, derivedWithStore, observableFromEvent, observableSignalFromEvent, observableValue, recomputeInitiallyAndOnChange } from '../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { PlaceholderViewZone, ViewZoneOverlayWidget, applyStyle, applyViewZones } from '../utils.js'; import { OffsetRange, OffsetRangeSet } from '../../../../common/core/offsetRange.js'; import { localize } from '../../../../../nls.js'; export class MovedBlocksLinesFeature extends Disposable { constructor(_rootElement, _diffModel, _originalEditorLayoutInfo, _modifiedEditorLayoutInfo, _editors) { super(); this._rootElement = _rootElement; this._diffModel = _diffModel; this._originalEditorLayoutInfo = _originalEditorLayoutInfo; this._modifiedEditorLayoutInfo = _modifiedEditorLayoutInfo; this._editors = _editors; this._originalScrollTop = observableFromEvent(this._editors.original.onDidScrollChange, () => this._editors.original.getScrollTop()); this._modifiedScrollTop = observableFromEvent(this._editors.modified.onDidScrollChange, () => this._editors.modified.getScrollTop()); this._viewZonesChanged = observableSignalFromEvent('onDidChangeViewZones', this._editors.modified.onDidChangeViewZones); this.width = observableValue(this, 0); this._modifiedViewZonesChangedSignal = observableSignalFromEvent('modified.onDidChangeViewZones', this._editors.modified.onDidChangeViewZones); this._originalViewZonesChangedSignal = observableSignalFromEvent('original.onDidChangeViewZones', this._editors.original.onDidChangeViewZones); this._state = derivedWithStore(this, (reader, store) => { /** @description state */ var _a; this._element.replaceChildren(); const model = this._diffModel.read(reader); const moves = (_a = model === null || model === void 0 ? void 0 : model.diff.read(reader)) === null || _a === void 0 ? void 0 : _a.movedTexts; if (!moves || moves.length === 0) { this.width.set(0, undefined); return; } this._viewZonesChanged.read(reader); const infoOrig = this._originalEditorLayoutInfo.read(reader); const infoMod = this._modifiedEditorLayoutInfo.read(reader); if (!infoOrig || !infoMod) { this.width.set(0, undefined); return; } this._modifiedViewZonesChangedSignal.read(reader); this._originalViewZonesChangedSignal.read(reader); const lines = moves.map((move) => { function computeLineStart(range, editor) { const t1 = editor.getTopForLineNumber(range.startLineNumber, true); const t2 = editor.getTopForLineNumber(range.endLineNumberExclusive, true); return (t1 + t2) / 2; } const start = computeLineStart(move.lineRangeMapping.original, this._editors.original); const startOffset = this._originalScrollTop.read(reader); const end = computeLineStart(move.lineRangeMapping.modified, this._editors.modified); const endOffset = this._modifiedScrollTop.read(reader); const from = start - startOffset; const to = end - endOffset; const top = Math.min(start, end); const bottom = Math.max(start, end); return { range: new OffsetRange(top, bottom), from, to, fromWithoutScroll: start, toWithoutScroll: end, move }; }); lines.sort(tieBreakComparators(compareBy(l => l.fromWithoutScroll > l.toWithoutScroll, booleanComparator), compareBy(l => l.fromWithoutScroll > l.toWithoutScroll ? l.fromWithoutScroll : -l.toWithoutScroll, numberComparator))); const layout = LinesLayout.compute(lines.map(l => l.range)); const padding = 10; const lineAreaLeft = infoOrig.verticalScrollbarWidth; const lineAreaWidth = (layout.getTrackCount() - 1) * 10 + padding * 2; const width = lineAreaLeft + lineAreaWidth + (infoMod.contentLeft - MovedBlocksLinesFeature.movedCodeBlockPadding); let idx = 0; for (const line of lines) { const track = layout.getTrack(idx); const verticalY = lineAreaLeft + padding + track * 10; const arrowHeight = 15; const arrowWidth = 15; const right = width; const rectWidth = infoMod.glyphMarginWidth + infoMod.lineNumbersWidth; const rectHeight = 18; const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); rect.classList.add('arrow-rectangle'); rect.setAttribute('x', `${right - rectWidth}`); rect.setAttribute('y', `${line.to - rectHeight / 2}`); rect.setAttribute('width', `${rectWidth}`); rect.setAttribute('height', `${rectHeight}`); this._element.appendChild(rect); const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', `M ${0} ${line.from} L ${verticalY} ${line.from} L ${verticalY} ${line.to} L ${right - arrowWidth} ${line.to}`); path.setAttribute('fill', 'none'); g.appendChild(path); const arrowRight = document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); arrowRight.classList.add('arrow'); store.add(autorun(reader => { path.classList.toggle('currentMove', line.move === model.activeMovedText.read(reader)); arrowRight.classList.toggle('currentMove', line.move === model.activeMovedText.read(reader)); })); arrowRight.setAttribute('points', `${right - arrowWidth},${line.to - arrowHeight / 2} ${right},${line.to} ${right - arrowWidth},${line.to + arrowHeight / 2}`); g.appendChild(arrowRight); this._element.appendChild(g); /* TODO@hediet path.addEventListener('mouseenter', () => { model.setHoveredMovedText(line.move); }); path.addEventListener('mouseleave', () => { model.setHoveredMovedText(undefined); });*/ idx++; } this.width.set(lineAreaWidth, undefined); }); this._element = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this._element.setAttribute('class', 'moved-blocks-lines'); this._rootElement.appendChild(this._element); this._register(toDisposable(() => this._element.remove())); this._register(autorun(reader => { /** @description update moved blocks lines positioning */ const info = this._originalEditorLayoutInfo.read(reader); const info2 = this._modifiedEditorLayoutInfo.read(reader); if (!info || !info2) { return; } this._element.style.left = `${info.width - info.verticalScrollbarWidth}px`; this._element.style.height = `${info.height}px`; this._element.style.width = `${info.verticalScrollbarWidth + info.contentLeft - MovedBlocksLinesFeature.movedCodeBlockPadding + this.width.read(reader)}px`; })); this._register(recomputeInitiallyAndOnChange(this._state)); const movedBlockViewZones = derived(reader => { const model = this._diffModel.read(reader); const d = model === null || model === void 0 ? void 0 : model.diff.read(reader); if (!d) { return []; } return d.movedTexts.map(move => ({ move, original: new PlaceholderViewZone(constObservable(move.lineRangeMapping.original.startLineNumber - 1), 18), modified: new PlaceholderViewZone(constObservable(move.lineRangeMapping.modified.startLineNumber - 1), 18), })); }); this._register(applyViewZones(this._editors.original, movedBlockViewZones.map(zones => /** @description movedBlockViewZones.original */ zones.map(z => z.original)))); this._register(applyViewZones(this._editors.modified, movedBlockViewZones.map(zones => /** @description movedBlockViewZones.modified */ zones.map(z => z.modified)))); this._register(autorunWithStore((reader, store) => { const blocks = movedBlockViewZones.read(reader); for (const b of blocks) { store.add(new MovedBlockOverlayWidget(this._editors.original, b.original, b.move, 'original', this._diffModel.get())); store.add(new MovedBlockOverlayWidget(this._editors.modified, b.modified, b.move, 'modified', this._diffModel.get())); } })); const originalCursorPosition = observableFromEvent(this._editors.original.onDidChangeCursorPosition, () => this._editors.original.getPosition()); const modifiedCursorPosition = observableFromEvent(this._editors.modified.onDidChangeCursorPosition, () => this._editors.modified.getPosition()); const originalHasFocus = observableSignalFromEvent('original.onDidFocusEditorWidget', e => this._editors.original.onDidFocusEditorWidget(() => setTimeout(() => e(undefined), 0))); const modifiedHasFocus = observableSignalFromEvent('modified.onDidFocusEditorWidget', e => this._editors.modified.onDidFocusEditorWidget(() => setTimeout(() => e(undefined), 0))); let lastChangedEditor = 'modified'; this._register(autorunHandleChanges({ createEmptyChangeSummary: () => undefined, handleChange: (ctx, summary) => { if (ctx.didChange(originalHasFocus)) { lastChangedEditor = 'original'; } if (ctx.didChange(modifiedHasFocus)) { lastChangedEditor = 'modified'; } return true; } }, reader => { /** @description MovedBlocksLines.setActiveMovedTextFromCursor */ originalHasFocus.read(reader); modifiedHasFocus.read(reader); const m = this._diffModel.read(reader); if (!m) { return; } const diff = m.diff.read(reader); let movedText = undefined; if (diff && lastChangedEditor === 'original') { const originalPos = originalCursorPosition.read(reader); if (originalPos) { movedText = diff.movedTexts.find(m => m.lineRangeMapping.original.contains(originalPos.lineNumber)); } } if (diff && lastChangedEditor === 'modified') { const modifiedPos = modifiedCursorPosition.read(reader); if (modifiedPos) { movedText = diff.movedTexts.find(m => m.lineRangeMapping.modified.contains(modifiedPos.lineNumber)); } } if (movedText !== m.movedTextToCompare.get()) { m.movedTextToCompare.set(undefined, undefined); } m.setActiveMovedText(movedText); })); } } MovedBlocksLinesFeature.movedCodeBlockPadding = 4; class LinesLayout { static compute(lines) { const setsPerTrack = []; const trackPerLineIdx = []; for (const line of lines) { let trackIdx = setsPerTrack.findIndex(set => !set.intersectsStrict(line)); if (trackIdx === -1) { const maxTrackCount = 6; if (setsPerTrack.length >= maxTrackCount) { trackIdx = findMaxIdxBy(setsPerTrack, compareBy(set => set.intersectWithRangeLength(line), numberComparator)); } else { trackIdx = setsPerTrack.length; setsPerTrack.push(new OffsetRangeSet()); } } setsPerTrack[trackIdx].addRange(line); trackPerLineIdx.push(trackIdx); } return new LinesLayout(setsPerTrack.length, trackPerLineIdx); } constructor(_trackCount, trackPerLineIdx) { this._trackCount = _trackCount; this.trackPerLineIdx = trackPerLineIdx; } getTrack(lineIdx) { return this.trackPerLineIdx[lineIdx]; } getTrackCount() { return this._trackCount; } } class MovedBlockOverlayWidget extends ViewZoneOverlayWidget { constructor(_editor, _viewZone, _move, _kind, _diffModel) { const root = h('div.diff-hidden-lines-widget'); super(_editor, _viewZone, root.root); this._editor = _editor; this._move = _move; this._kind = _kind; this._diffModel = _diffModel; this._nodes = h('div.diff-moved-code-block', { style: { marginRight: '4px' } }, [ h('div.text-content@textContent'), h('div.action-bar@actionBar'), ]); root.root.appendChild(this._nodes.root); const editorLayout = observableFromEvent(this._editor.onDidLayoutChange, () => this._editor.getLayoutInfo()); this._register(applyStyle(this._nodes.root, { paddingRight: editorLayout.map(l => l.verticalScrollbarWidth) })); let text; if (_move.changes.length > 0) { text = this._kind === 'original' ? localize('codeMovedToWithChanges', 'Code moved with changes to line {0}-{1}', this._move.lineRangeMapping.modified.startLineNumber, this._move.lineRangeMapping.modified.endLineNumberExclusive - 1) : localize('codeMovedFromWithChanges', 'Code moved with changes from line {0}-{1}', this._move.lineRangeMapping.original.startLineNumber, this._move.lineRangeMapping.original.endLineNumberExclusive - 1); } else { text = this._kind === 'original' ? localize('codeMovedTo', 'Code moved to line {0}-{1}', this._move.lineRangeMapping.modified.startLineNumber, this._move.lineRangeMapping.modified.endLineNumberExclusive - 1) : localize('codeMovedFrom', 'Code moved from line {0}-{1}', this._move.lineRangeMapping.original.startLineNumber, this._move.lineRangeMapping.original.endLineNumberExclusive - 1); } const actionBar = this._register(new ActionBar(this._nodes.actionBar, { highlightToggledItems: true, })); const caption = new Action('', text, '', false); actionBar.push(caption, { icon: false, label: true }); const actionCompare = new Action('', 'Compare', ThemeIcon.asClassName(Codicon.compareChanges), true, () => { this._editor.focus(); this._diffModel.movedTextToCompare.set(this._diffModel.movedTextToCompare.get() === _move ? undefined : this._move, undefined); }); this._register(autorun(reader => { const isActive = this._diffModel.movedTextToCompare.read(reader) === _move; actionCompare.checked = isActive; })); actionBar.push(actionCompare, { icon: false, label: true }); } }