1 // Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.diff.tools.simple
4 import com.intellij.diff.tools.simple.SimpleAlignedDiffModel.ChangeIntersection.*
5 import com.intellij.diff.util.*
6 import com.intellij.openapi.Disposable
7 import com.intellij.openapi.editor.*
8 import com.intellij.openapi.editor.ex.EditorEx
9 import com.intellij.openapi.editor.ex.RangeMarkerEx
10 import com.intellij.openapi.editor.impl.EditorImpl
11 import com.intellij.openapi.editor.markup.HighlighterLayer
12 import com.intellij.openapi.editor.markup.HighlighterTargetArea
13 import com.intellij.openapi.editor.markup.RangeHighlighter
14 import com.intellij.openapi.editor.markup.TextAttributes
15 import com.intellij.openapi.util.Disposer
16 import com.intellij.ui.ColorUtil
18 import java.awt.Graphics
19 import java.awt.Rectangle
20 import kotlin.math.abs
22 class SimpleAlignedDiffModel(private val viewer: SimpleDiffViewer) {
24 * Changes mapped to corresponding change aligning inlays (INSERTED, DELETED, MODIFIED changes).
26 private val alignedInlays = mutableMapOf<SideAndChange, Inlay<ChangeAlignDiffInlayPresentation>>()
29 * Inlay lines mapped to corresponding aligning empty line inlay from other diff side.
31 private val emptyInlays = mutableMapOf<InlayId, Inlay<EmptyLineAlignDiffInlayPresentation>>()
32 private val adjustedInlaysHeights = mutableMapOf<SideAndChange, Int>()
33 private val inlayHighlighters = mutableMapOf<Side, MutableList<RangeHighlighter>>()
36 if (viewer.needAlignChanges()) {
37 val inlayListener = MyInlayModelListener()
38 viewer.getEditor(Side.LEFT).inlayModel.addListener(inlayListener, viewer)
39 viewer.getEditor(Side.RIGHT).inlayModel.addListener(inlayListener, viewer)
43 fun alignChange(change: SimpleDiffChange) {
44 if (!viewer.needAlignChanges()) return
46 when (change.diffType) {
47 TextDiffType.INSERTED -> {
48 addInlay(change, TextDiffType.INSERTED, Side.LEFT)
50 TextDiffType.DELETED -> {
51 addInlay(change, TextDiffType.DELETED, Side.RIGHT)
53 TextDiffType.MODIFIED -> {
54 addInlay(change, TextDiffType.MODIFIED, Side.LEFT)
55 addInlay(change, TextDiffType.MODIFIED, Side.RIGHT)
60 private fun addInlay(change: SimpleDiffChange, diffType: TextDiffType, inlaySide: Side) {
61 val changeSide = inlaySide.other()
63 val changeStartLine = change.getStartLine(changeSide)
64 val changeEndLine = change.getEndLine(changeSide)
65 val inlayStartLine = change.getStartLine(inlaySide)
66 val inlayEndLine = change.getEndLine(inlaySide)
67 val isLastLine = changeEndLine == DiffUtil.getLineCount(viewer.getEditor(changeSide).document)
69 val delta = (changeEndLine - changeStartLine) - (inlayEndLine - inlayStartLine)
70 if (delta <= 0) return
72 createAlignInlay(inlaySide, change, delta, isLastLine)
73 .also { createInlayHighlighter(inlaySide, it, diffType, isLastLine) }
74 .also { alignedInlays[SideAndChange(inlaySide, change)] = it }
77 private fun createInlayHighlighter(side: Side, inlay: Inlay<*>, type: TextDiffType, isLastLine: Boolean) {
78 val editor = viewer.getEditor(side)
79 val startOffset = inlay.offset
80 val endOffset = if (inlay is RangeMarker) inlay.endOffset else startOffset
82 val highlighter = editor.markupModel
83 .addRangeHighlighter(startOffset, endOffset, HighlighterLayer.SELECTION, TextAttributes(), HighlighterTargetArea.EXACT_RANGE)
84 if (type != TextDiffType.MODIFIED) {
85 highlighter.lineMarkerRenderer = DiffInlayMarkerRenderer(type, inlay, isLastLine)
87 inlayHighlighters.getOrPut(side) { mutableListOf() }.add(highlighter)
90 private fun createAlignInlay(side: Side,
91 change: SimpleDiffChange,
93 isLastLineToAdd: Boolean): Inlay<ChangeAlignDiffInlayPresentation> {
94 val editor = viewer.getEditor(side)
95 val offset = DiffUtil.getOffset(editor.document, change.getStartLine(side), 0)
96 val inlayPresentation = adjustedInlaysHeights.entries
97 .find { it.key.side == side && it.key.change.isSame(change)}
98 ?.let { ChangeAlignDiffInlayPresentation(editor, it.value, change.diffType) }
99 ?: ChangeAlignDiffInlayPresentation(editor, editor.lineHeight * linesToAdd, change.diffType)
101 return editor.inlayModel
102 .addBlockElement(offset,
104 .showAbove(!isLastLineToAdd)
105 .priority(ALIGNED_CHANGE_INLAY_PRIORITY),
110 alignedInlays.values.forEach(Disposer::dispose)
111 alignedInlays.clear()
112 for ((side, highlighters) in inlayHighlighters) {
113 val markupModel = viewer.getEditor(side).markupModel
114 highlighters.forEach(markupModel::removeHighlighter)
116 inlayHighlighters.clear()
119 private open class BaseAlignDiffInlayPresentation(private val editor: EditorEx,
122 private val inlayColor: Color? = null) : EditorCustomElementRenderer {
124 override fun paint(inlay: Inlay<*>, g: Graphics, targetRegion: Rectangle, textAttributes: TextAttributes) {
126 val paintColor = inlayColor ?: return
129 g.fillRect(targetRegion.x, targetRegion.y, editor.preferredSize.width, height)
132 override fun calcWidthInPixels(inlay: Inlay<*>): Int = width
134 override fun calcHeightInPixels(inlay: Inlay<*>): Int = height
137 private class EmptyLineAlignDiffInlayPresentation(editor: EditorEx, height: Int, inlayColor: Color? = editor.backgroundColor) :
138 BaseAlignDiffInlayPresentation(editor, height, editor.component.width, inlayColor)
140 private class ChangeAlignDiffInlayPresentation(editor: EditorEx, height: Int, diffType: TextDiffType) :
141 BaseAlignDiffInlayPresentation(editor, height, editor.component.width, getAlignedChangeColor(diffType, editor))
143 private inner class MyInlayModelListener : InlayModel.Listener {
144 override fun onAdded(inlay: Inlay<*>) = processInlay(inlay, ProcessType.ADDED)
146 override fun onRemoved(inlay: Inlay<*>) = processInlay(inlay, ProcessType.REMOVED)
148 override fun onUpdated(inlay: Inlay<*>, changeFlags: Int) {
149 if (changeFlags and InlayModel.ChangeFlags.HEIGHT_CHANGED != 0) {
150 processInlay(inlay, ProcessType.HEIGHT_UPDATED)
155 private enum class ProcessType { ADDED, REMOVED, HEIGHT_UPDATED }
157 private fun processInlay(inlay: Inlay<*>, processType: ProcessType) {
158 if (inlay.renderer is BaseAlignDiffInlayPresentation) return //skip self
160 val inlayLine = inlay.logicalLine
161 val inlaySide = if (viewer.getEditor(Side.LEFT) == inlay.editor) Side.LEFT else Side.RIGHT
163 if (needSkipInlay(inlay, inlaySide)) return
165 val alignSide = inlaySide.other()
166 val isAboveInlay = inlay.properties.isShownAbove
167 val lineToBeAligned = getRelatedLogicalLine(inlaySide, inlayLine, isAboveInlay)
168 val changeIntersection = getChangeIntersection(inlaySide, inlayLine)
169 val inlayId = InlayId(alignSide, inlay.offset, inlay.id)
172 ProcessType.REMOVED -> {
173 changeAlignedInlayHeight(changeIntersection, alignSide) { affectedInlay ->
174 affectedInlay.renderer.height - inlay.heightInPixels
176 emptyInlays.remove(inlayId)?.let(Disposer::dispose)
178 ProcessType.HEIGHT_UPDATED -> {
179 changeAlignedInlayHeight(changeIntersection, alignSide) {
180 (changeIntersection as InsideChange).change.calculateDeltaHeight() + inlay.heightInPixels
182 emptyInlays[inlayId]?.run { renderer.height = inlay.heightInPixels; update() }
184 ProcessType.ADDED -> {
185 when (changeIntersection) {
187 val alignInlayPriority = if (isAboveInlay) ALIGNED_CHANGE_INLAY_PRIORITY else Int.MIN_VALUE
188 addEmptyInlay(inlayId, lineToBeAligned, inlay.heightInPixels, isAboveInlay, alignInlayPriority, parent = inlay)
191 changeAlignedInlayHeight(changeIntersection, alignSide) { affectedInlay ->
192 affectedInlay.renderer.height + inlay.heightInPixels
194 val change = changeIntersection.change
195 if (!alignedInlays.containsKey(SideAndChange(alignSide, change))) {
196 val alignInlayPriority = if (isAboveInlay) ALIGNED_CHANGE_INLAY_PRIORITY else Int.MIN_VALUE
197 val color = change.diffType.getColor(inlay.editor)
198 addEmptyInlay(inlayId, lineToBeAligned, inlay.heightInPixels, isAboveInlay, alignInlayPriority, color, parent = inlay)
201 is NoIntersection -> {
202 addEmptyInlay(inlayId, lineToBeAligned, inlay.heightInPixels, isAboveInlay, Int.MAX_VALUE, parent = inlay)
209 private fun needSkipInlay(inlay: Inlay<*>,
210 inlaySide: Side): Boolean {
211 val alignSide = inlaySide.other()
212 val inlayEditor = viewer.getEditor(inlaySide)
213 val alignEditor = viewer.getEditor(alignSide)
214 //inlays added below line not supported, except last line
215 return !inlay.properties.isShownAbove && inlay.onLastLine &&
216 DiffUtil.getLineCount(alignEditor.document) <= DiffUtil.getLineCount(inlayEditor.document)
219 private fun changeAlignedInlayHeight(changeIntersection: ChangeIntersection, side: Side,
220 heightCalculator: (Inlay<ChangeAlignDiffInlayPresentation>) -> Int) {
221 if (changeIntersection !is InsideChange) return
223 val change = changeIntersection.change
225 val sideAndChange = SideAndChange(side, change)
226 alignedInlays[sideAndChange]
228 renderer.height = heightCalculator(this)
229 adjustedInlaysHeights[sideAndChange] = renderer.height
234 private fun addEmptyInlay(inlayId: InlayId, line: Int, height: Int, above: Boolean, priority: Int,
235 color: Color = viewer.getEditor(inlayId.side.other()).backgroundColor, parent: Disposable) {
236 val editor = viewer.getEditor(inlayId.side)
237 val offset = DiffUtil.getOffset(editor.document, line, 0)
238 val disposable = Disposable { emptyInlays.remove(inlayId)?.also(Disposer::dispose) }
240 emptyInlays[inlayId] =
241 editor.inlayModel.addBlockElement(offset,
242 InlayProperties().showAbove(above).priority(priority), EmptyLineAlignDiffInlayPresentation(editor, height, color))!!
243 .also { Disposer.register(parent, disposable) }
246 private fun getChangeIntersection(side: Side, logicalLine: Int): ChangeIntersection {
247 for (change in viewer.diffChanges) {
249 change.isStartLine(side, logicalLine) -> return AboveChange
250 change.isMiddleLine(side, logicalLine) -> return InsideChange(change)
254 return NoIntersection
257 private fun SimpleDiffChange.isStartLine(side: Side, logicalLine: Int) = getStartLine(side) == logicalLine
258 private fun SimpleDiffChange.isMiddleLine(side: Side, logicalLine: Int) =
259 getStartLine(side) < logicalLine && getEndLine(side) - 1 >= logicalLine
261 private sealed class ChangeIntersection {
262 class InsideChange(val change: SimpleDiffChange) : ChangeIntersection()
263 object AboveChange : ChangeIntersection()
264 object NoIntersection : ChangeIntersection()
267 private data class InlayId(val side: Side, val offset: Int, val id: Long)
268 private data class SideAndChange(val side: Side, val change: SimpleDiffChange)
270 private fun getRelatedLogicalLine(side: Side, logicalLine: Int, isAboveInlay: Boolean): Int {
271 val needAlignLastLine = logicalLine == DiffUtil.getLineCount(viewer.getEditor(side).document) - 1
273 if (needAlignLastLine && !isAboveInlay) {
274 // for last line and below added inlay, related line should be always the last line (if there is no change intersection)
275 val alignSide = side.other()
276 val lastLine = DiffUtil.getLineCount(viewer.getEditor(alignSide).document) - 1
277 val changeIntersection = getChangeIntersection(alignSide, lastLine)
278 if (changeIntersection == NoIntersection) {
283 return viewer.transferPosition(side, LineCol(logicalLine, 0)).line
286 private val Inlay<*>.onLastLine get() = DiffUtil.getLineCount(editor.document) - 1 == logicalLine
287 private val Inlay<*>.logicalLine get() = editor.offsetToLogicalPosition(offset).line
288 private val Inlay<*>.id get() = (this as RangeMarkerEx).id
290 private fun SimpleDiffChange.isSame(other: SimpleDiffChange) =
291 getStartLine(Side.LEFT) == other.getStartLine(Side.LEFT) && getEndLine(Side.LEFT) == other.getEndLine(Side.LEFT) &&
292 getStartLine(Side.RIGHT) == other.getStartLine(Side.RIGHT) && getEndLine(Side.RIGHT) == other.getEndLine(Side.RIGHT)
294 private fun SimpleDiffChange.calculateDeltaHeight(): Int {
295 val leftStartLine = getStartLine(Side.LEFT)
296 val leftEndLine = getEndLine(Side.LEFT)
297 val rightStartLine = getStartLine(Side.RIGHT)
298 val rightEndLine = getEndLine(Side.RIGHT)
300 val delta = (leftEndLine - leftStartLine) - (rightEndLine - rightStartLine)
302 return abs(delta) * viewer.getEditor(Side.LEFT).lineHeight
306 const val ALIGNED_CHANGE_INLAY_PRIORITY = 0
308 fun getAlignedChangeColor(type: TextDiffType, editor: Editor): Color? {
309 return if (type === TextDiffType.MODIFIED) null else type.getColor(editor).let { ColorUtil.toAlpha(it, 200) }