[duplicates] enable duplicates analysis in PyCharm/WebStorm/PhpStorm/RubyMine
[idea/community.git] / platform / diff-impl / tests / com / intellij / diff / merge / MergeTestBase.kt
1 // Copyright 2000-2019 JetBrains s.r.o. 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.merge
3
4 import com.intellij.diff.DiffContentFactoryImpl
5 import com.intellij.diff.HeavyDiffTestCase
6 import com.intellij.diff.contents.DocumentContent
7 import com.intellij.diff.merge.MergeTestBase.SidesState.*
8 import com.intellij.diff.merge.TextMergeViewer.MyThreesideViewer
9 import com.intellij.diff.tools.util.base.IgnorePolicy
10 import com.intellij.diff.tools.util.base.TextDiffSettingsHolder.TextDiffSettings
11 import com.intellij.diff.util.*
12 import com.intellij.openapi.actionSystem.ActionPlaces
13 import com.intellij.openapi.actionSystem.AnAction
14 import com.intellij.openapi.actionSystem.AnActionEvent
15 import com.intellij.openapi.application.ApplicationManager
16 import com.intellij.openapi.command.CommandProcessor
17 import com.intellij.openapi.command.undo.UndoManager
18 import com.intellij.openapi.editor.Document
19 import com.intellij.openapi.editor.ex.EditorEx
20 import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider
21 import com.intellij.openapi.project.Project
22 import com.intellij.openapi.util.Couple
23 import com.intellij.openapi.util.Disposer
24 import com.intellij.openapi.util.text.StringUtil
25 import com.intellij.util.ui.UIUtil
26
27 abstract class MergeTestBase : HeavyDiffTestCase() {
28   fun test1(left: String, base: String, right: String, f: TestBuilder.() -> Unit) {
29     test(left, base, right, 1, f)
30   }
31
32   fun test2(left: String, base: String, right: String, f: TestBuilder.() -> Unit) {
33     test(left, base, right, 2, f)
34   }
35
36   fun testN(left: String, base: String, right: String, f: TestBuilder.() -> Unit) {
37     test(left, base, right, -1, f)
38   }
39
40   fun test(left: String, base: String, right: String, changesCount: Int, f: TestBuilder.() -> Unit) {
41     test(left, base, right, changesCount, IgnorePolicy.DEFAULT, f)
42   }
43
44   fun test(left: String, base: String, right: String, changesCount: Int, policy: IgnorePolicy, f: TestBuilder.() -> Unit) {
45     val contentFactory = DiffContentFactoryImpl()
46     val leftContent: DocumentContent = contentFactory.create(parseSource(left))
47     val baseContent: DocumentContent = contentFactory.create(parseSource(base))
48     val rightContent: DocumentContent = contentFactory.create(parseSource(right))
49     val outputContent: DocumentContent = contentFactory.create(parseSource(""))
50     outputContent.document.setReadOnly(false)
51
52     val context = MockMergeContext(project)
53     val request = MockMergeRequest(leftContent, baseContent, rightContent, outputContent)
54
55     val settings = TextDiffSettings()
56     settings.ignorePolicy = policy
57     context.putUserData(TextDiffSettings.KEY, settings)
58
59     val viewer = TextMergeTool.INSTANCE.createComponent(context, request) as TextMergeViewer
60     try {
61       val toolbar = viewer.init()
62       UIUtil.dispatchAllInvocationEvents()
63
64       val builder = TestBuilder(viewer, toolbar.toolbarActions ?: emptyList())
65       builder.assertChangesCount(changesCount)
66       builder.f()
67     }
68     finally {
69       Disposer.dispose(viewer)
70     }
71   }
72
73   inner class TestBuilder(val mergeViewer: TextMergeViewer, private val actions: List<AnAction>) {
74     val viewer: MyThreesideViewer = mergeViewer.viewer
75     val changes: List<TextMergeChange> = viewer.allChanges
76     val editor: EditorEx = viewer.editor
77     val document: Document = editor.document
78
79     private val textEditor = TextEditorProvider.getInstance().getTextEditor(editor)
80     private val undoManager = UndoManager.getInstance(project!!)
81
82     fun change(num: Int): TextMergeChange {
83       if (changes.size < num) throw Exception("changes: ${changes.size}, index: $num")
84       return changes[num]
85     }
86
87     fun activeChanges(): List<TextMergeChange> = viewer.changes
88
89     //
90     // Actions
91     //
92
93     fun runApplyNonConflictsAction(side: ThreeSide) {
94       runActionById(side.select("Left", "All", "Right")!!)
95     }
96
97     private fun runActionById(text: String): Boolean {
98       val action = actions.filter { text == it.templatePresentation.text }.single()
99       return runAction(action)
100     }
101
102     private fun runAction(action: AnAction): Boolean {
103       val actionEvent = AnActionEvent.createFromAnAction(action, null, ActionPlaces.MAIN_MENU, editor.dataContext)
104       action.update(actionEvent)
105       val success = actionEvent.presentation.isEnabledAndVisible
106       if (success) action.actionPerformed(actionEvent)
107       return success
108     }
109
110     //
111     // Modification
112     //
113
114     fun command(affected: TextMergeChange, f: () -> Unit) {
115       command(listOf(affected), f)
116     }
117
118     fun command(affected: List<TextMergeChange>? = null, f: () -> Unit) {
119       viewer.executeMergeCommand(null, affected, f)
120       UIUtil.dispatchAllInvocationEvents()
121     }
122
123     fun write(f: () -> Unit) {
124       ApplicationManager.getApplication().runWriteAction { CommandProcessor.getInstance().executeCommand(project, f, null, null) }
125     }
126
127     fun Int.ignore(side: Side, modifier: Boolean = false) {
128       val change = change(this)
129       command(change) { viewer.ignoreChange(change, side, modifier) }
130     }
131
132     fun Int.apply(side: Side, modifier: Boolean = false) {
133       val change = change(this)
134       command(change) { viewer.replaceChange(change, side, modifier) }
135     }
136
137     fun Int.resolve() {
138       val change = change(this)
139       command(change) {
140         assertTrue(change.isConflict && viewer.canResolveChangeAutomatically(change, ThreeSide.BASE))
141         viewer.resolveChangeAutomatically(change, ThreeSide.BASE)
142       }
143     }
144
145     fun Int.canResolveConflict(): Boolean {
146       val change = change(this)
147       return viewer.canResolveChangeAutomatically(change, ThreeSide.BASE)
148     }
149
150     //
151     // Text modification
152     //
153
154     fun insertText(offset: Int, newContent: CharSequence) {
155       replaceText(offset, offset, newContent)
156     }
157
158     fun deleteText(startOffset: Int, endOffset: Int) {
159       replaceText(startOffset, endOffset, "")
160     }
161
162     fun replaceText(startOffset: Int, endOffset: Int, newContent: CharSequence) {
163       write { document.replaceString(startOffset, endOffset, parseSource(newContent)) }
164     }
165
166     fun insertText(offset: LineCol, newContent: CharSequence) {
167       replaceText(offset.toOffset(), offset.toOffset(), newContent)
168     }
169
170     fun deleteText(startOffset: LineCol, endOffset: LineCol) {
171       replaceText(startOffset.toOffset(), endOffset.toOffset(), "")
172     }
173
174     fun replaceText(startOffset: LineCol, endOffset: LineCol, newContent: CharSequence) {
175       write { replaceText(startOffset.toOffset(), endOffset.toOffset(), newContent) }
176     }
177
178     fun replaceText(oldContent: CharSequence, newContent: CharSequence) {
179       write {
180         val range = findRange(parseSource(oldContent))
181         replaceText(range.first, range.second, newContent)
182       }
183     }
184
185     fun deleteText(oldContent: CharSequence) {
186       write {
187         val range = findRange(parseSource(oldContent))
188         replaceText(range.first, range.second, "")
189       }
190     }
191
192     fun insertTextBefore(oldContent: CharSequence, newContent: CharSequence) {
193       write { insertText(findRange(parseSource(oldContent)).first, newContent) }
194     }
195
196     fun insertTextAfter(oldContent: CharSequence, newContent: CharSequence) {
197       write { insertText(findRange(parseSource(oldContent)).second, newContent) }
198     }
199
200     private fun findRange(oldContent: CharSequence): Couple<Int> {
201       val text = document.charsSequence
202       val index1 = StringUtil.indexOf(text, oldContent)
203       assertTrue(index1 >= 0, "content - '\n$oldContent\n'\ntext - '\n$text'")
204       val index2 = StringUtil.indexOf(text, oldContent, index1 + 1)
205       assertTrue(index2 == -1, "content - '\n$oldContent\n'\ntext - '\n$text'")
206       return Couple(index1, index1 + oldContent.length)
207     }
208
209     //
210     // Undo
211     //
212
213     fun assertCantUndo() {
214       assertFalse(undoManager.isUndoAvailable(textEditor))
215     }
216
217     fun undo(count: Int = 1) {
218       if (count == -1) {
219         while (undoManager.isUndoAvailable(textEditor)) {
220           undoManager.undo(textEditor)
221         }
222       }
223       else {
224         for (i in 1..count) {
225           assertTrue(undoManager.isUndoAvailable(textEditor))
226           undoManager.undo(textEditor)
227         }
228       }
229     }
230
231     fun redo(count: Int = 1) {
232       if (count == -1) {
233         while (undoManager.isRedoAvailable(textEditor)) {
234           undoManager.redo(textEditor)
235         }
236       }
237       else {
238         for (i in 1..count) {
239           assertTrue(undoManager.isRedoAvailable(textEditor))
240           undoManager.redo(textEditor)
241         }
242       }
243     }
244
245     fun checkUndo(count: Int = -1, f: TestBuilder.() -> Unit) {
246       val initialState = ViewerState.recordState(viewer)
247       f()
248       UIUtil.dispatchAllInvocationEvents()
249
250       val afterState = ViewerState.recordState(viewer)
251       undo(count)
252       UIUtil.dispatchAllInvocationEvents()
253
254       val undoState = ViewerState.recordState(viewer)
255       redo(count)
256       UIUtil.dispatchAllInvocationEvents()
257
258       val redoState = ViewerState.recordState(viewer)
259
260       assertEquals(initialState, undoState)
261       assertEquals(afterState, redoState)
262     }
263
264     //
265     // Checks
266     //
267
268     fun assertChangesCount(expected: Int) {
269       if (expected == -1) return
270       val actual = activeChanges().size
271       assertEquals(expected, actual)
272     }
273
274     fun Int.assertType(type: TextDiffType, changeType: SidesState) {
275       assertType(type)
276       assertType(changeType)
277     }
278
279     fun Int.assertType(type: TextDiffType) {
280       val change = change(this)
281       assertEquals(change.diffType, type)
282     }
283
284     fun Int.assertType(changeType: SidesState) {
285       assertTrue(changeType != NONE)
286       val change = change(this)
287       val actual = change.type
288       val isLeftChange = changeType != RIGHT
289       val isRightChange = changeType != LEFT
290       assertEquals(Pair(isLeftChange, isRightChange), Pair(actual.isChange(Side.LEFT), actual.isChange(Side.RIGHT)))
291     }
292
293     fun Int.assertResolved(type: SidesState) {
294       val change = change(this)
295       val isLeftResolved = type == LEFT || type == BOTH
296       val isRightResolved = type == RIGHT || type == BOTH
297       assertEquals(Pair(isLeftResolved, isRightResolved), Pair(change.isResolved(Side.LEFT), change.isResolved(Side.RIGHT)))
298     }
299
300     fun Int.assertRange(start: Int, end: Int) {
301       val change = change(this)
302       assertEquals(Pair(start, end), Pair(change.startLine, change.endLine))
303     }
304
305     fun Int.assertRange(start1: Int, end1: Int, start2: Int, end2: Int, start3: Int, end3: Int) {
306       val change = change(this)
307       assertEquals(MergeRange(start1, end1, start2, end2, start3, end3),
308                    MergeRange(change.getStartLine(ThreeSide.LEFT), change.getEndLine(ThreeSide.LEFT),
309                               change.getStartLine(ThreeSide.BASE), change.getEndLine(ThreeSide.BASE),
310                               change.getStartLine(ThreeSide.RIGHT), change.getEndLine(ThreeSide.RIGHT)))
311     }
312
313     fun Int.assertContent(expected: String, start: Int, end: Int) {
314       assertContent(expected)
315       assertRange(start, end)
316     }
317
318     fun Int.assertContent(expected: String) {
319       val change = change(this)
320       val document = editor.document
321       val actual = DiffUtil.getLinesContent(document, change.startLine, change.endLine)
322       assertEquals(parseSource(expected), actual)
323     }
324
325     fun assertContent(expected: String) {
326       val actual = viewer.editor.document.charsSequence
327       assertEquals(parseSource(expected), actual)
328     }
329
330     //
331     // Helpers
332     //
333
334     operator fun Int.not(): LineColHelper = LineColHelper(this)
335     operator fun LineColHelper.minus(col: Int): LineCol = LineCol(this.line, col)
336
337     inner class LineColHelper(val line: Int)
338
339     inner class LineCol(val line: Int, val col: Int) {
340       fun toOffset(): Int = editor.document.getLineStartOffset(line) + col
341     }
342   }
343
344   private class MockMergeContext(private val myProject: Project?) : MergeContext() {
345     override fun getProject(): Project? = myProject
346
347     override fun isFocusedInWindow(): Boolean = false
348
349     override fun requestFocusInWindow() {
350     }
351
352     override fun finishMerge(result: MergeResult) {
353     }
354   }
355
356   private class MockMergeRequest(val left: DocumentContent,
357                                  val base: DocumentContent,
358                                  val right: DocumentContent,
359                                  val output: DocumentContent) : TextMergeRequest() {
360     override fun getTitle(): String? = null
361
362     override fun applyResult(result: MergeResult) {
363     }
364
365     override fun getContents(): List<DocumentContent> = listOf(left, base, right)
366
367     override fun getOutputContent(): DocumentContent = output
368
369     override fun getContentTitles(): List<String?> = listOf(null, null, null)
370   }
371
372   enum class SidesState {
373     LEFT, RIGHT, BOTH, NONE
374   }
375
376   private data class ViewerState constructor(private val content: CharSequence,
377                                              private val changes: List<ViewerState.ChangeState>) {
378     companion object {
379       fun recordState(viewer: MyThreesideViewer): ViewerState {
380         val content = viewer.editor.document.immutableCharSequence
381         val changes = viewer.allChanges.map { recordChangeState(viewer, it) }
382         return ViewerState(content, changes)
383       }
384
385       private fun recordChangeState(viewer: MyThreesideViewer, change: TextMergeChange): ChangeState {
386         val document = viewer.editor.document
387         val content = DiffUtil.getLinesContent(document, change.startLine, change.endLine)
388
389         val resolved =
390           if (change.isResolved) BOTH
391           else if (change.isResolved(Side.LEFT)) LEFT
392           else if (change.isResolved(Side.RIGHT)) RIGHT
393           else NONE
394
395         val starts = Trio.from { change.getStartLine(it) }
396         val ends = Trio.from { change.getStartLine(it) }
397
398         return ChangeState(content, starts, ends, resolved)
399       }
400     }
401
402     override fun equals(other: Any?): Boolean {
403       if (this === other) return true
404       if (other !is ViewerState) return false
405
406       if (!StringUtil.equals(content, other.content)) return false
407       if (changes != other.changes) return false
408       return true
409     }
410
411     override fun hashCode(): Int = StringUtil.stringHashCode(content)
412
413     private data class ChangeState(private val content: CharSequence,
414                                    private val starts: Trio<Int>,
415                                    private val ends: Trio<Int>,
416                                    private val resolved: SidesState) {
417       override fun equals(other: Any?): Boolean {
418         if (this === other) return true
419         if (other !is ChangeState) return false
420
421         if (!StringUtil.equals(content, other.content)) return false
422         if (starts != other.starts) return false
423         if (ends != other.ends) return false
424         if (resolved != other.resolved) return false
425         return true
426       }
427
428       override fun hashCode(): Int = StringUtil.stringHashCode(content)
429     }
430   }
431 }