d8c6396babe8a279a6cf4d9edd6e7f49bfecdd50
[idea/contrib.git] / ide-features-trainer / src / training / learn / lesson / kimpl / LessonUtil.kt
1 // Copyright 2000-2020 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 training.learn.lesson.kimpl
3
4 import com.intellij.openapi.actionSystem.ActionManager
5 import com.intellij.openapi.application.ApplicationNamesInfo
6 import com.intellij.openapi.editor.Editor
7 import com.intellij.openapi.editor.EditorModificationUtil
8 import com.intellij.openapi.editor.ex.EditorEx
9 import com.intellij.openapi.fileEditor.FileDocumentManager
10 import com.intellij.openapi.util.NlsActions
11 import com.intellij.openapi.util.SystemInfo
12 import com.intellij.openapi.util.text.TextWithMnemonic
13 import com.intellij.openapi.wm.ToolWindow
14 import com.intellij.openapi.wm.ex.ToolWindowManagerListener
15 import com.intellij.util.messages.Topic
16 import com.intellij.xdebugger.XDebuggerManager
17 import training.commands.kotlin.TaskContext
18 import training.commands.kotlin.TaskRuntimeContext
19 import training.keymap.KeymapUtil
20 import training.learn.LearnBundle
21 import java.awt.event.KeyEvent
22 import javax.swing.JList
23 import javax.swing.KeyStroke
24
25 object LessonUtil {
26   val productName: String
27     get() = ApplicationNamesInfo.getInstance().fullProductName
28
29   fun insertIntoSample(sample: LessonSample, inserted: String): String {
30     return sample.text.substring(0, sample.startOffset) + inserted + sample.text.substring(sample.startOffset)
31   }
32
33   fun TaskRuntimeContext.checkPositionOfEditor(sample: LessonSample): TaskContext.RestoreNotification? {
34     fun invalidCaret(): Boolean {
35       val selection = sample.selection
36       val currentCaret = editor.caretModel.currentCaret
37       return if (selection != null && selection.first != selection.second) {
38         currentCaret.selectionStart != selection.first || currentCaret.selectionEnd != selection.second
39       }
40       else currentCaret.offset != sample.startOffset
41     }
42
43     return checkExpectedStateOfEditor(sample, false)
44            ?: if (invalidCaret()) sampleRestoreNotification(TaskContext.CaretRestoreProposal, sample) else null
45   }
46
47   fun TaskRuntimeContext.checkExpectedStateOfEditor(sample: LessonSample,
48                                                     checkPosition: Boolean = true,
49                                                     checkModification: (String) -> Boolean = { it.isEmpty() }): TaskContext.RestoreNotification? {
50     val prefix = sample.text.substring(0, sample.startOffset)
51     val postfix = sample.text.substring(sample.startOffset)
52
53     val docText = editor.document.charsSequence
54     val message = if (docText.startsWith(prefix) && docText.endsWith(postfix)) {
55       val middle = docText.subSequence(prefix.length, docText.length - postfix.length).toString()
56       if (checkModification(middle)) {
57         val offset = editor.caretModel.offset
58         if (!checkPosition || (prefix.length <= offset && offset <= prefix.length + middle.length)) {
59           null
60         }
61         else {
62           TaskContext.CaretRestoreProposal
63         }
64       }
65       else {
66         TaskContext.ModificationRestoreProposal
67       }
68     }
69     else {
70       TaskContext.ModificationRestoreProposal
71     }
72
73     return if (message != null) sampleRestoreNotification(message, sample) else null
74   }
75
76   fun TaskRuntimeContext.sampleRestoreNotification(message: String, sample: LessonSample) =
77     TaskContext.RestoreNotification(message) { setSample(sample) }
78
79   fun findItem(ui: JList<*>, checkList: (item: Any) -> Boolean): Int? {
80     for (i in 0 until ui.model.size) {
81       val elementAt = ui.model.getElementAt(i)
82       if (checkList(elementAt)) {
83         return i
84       }
85     }
86     return null
87   }
88
89   fun setEditorReadOnly(editor: Editor) {
90     if (editor !is EditorEx) return
91     editor.isViewer = true
92     EditorModificationUtil.setReadOnlyHint(editor, LearnBundle.message("learn.task.read.only.hint"))
93   }
94
95   fun actionName(actionId: String): @NlsActions.ActionText String {
96     val name = ActionManager.getInstance().getAction(actionId).templatePresentation.text ?: error("No action with ID $actionId")
97     return "<strong>${name}</strong>"
98   }
99
100   fun rawEnter(): String {
101     val keyStrokeEnter = KeymapUtil.getKeyStrokeText(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0))
102     return "<raw_action>$keyStrokeEnter</raw_action>"
103   }
104
105   fun rawCtrlEnter(): String {
106     return "<raw_action>${if (SystemInfo.isMacOSMojave) "\u2318\u23CE" else "Ctrl + Enter"}</raw_action>"
107   }
108 }
109
110 fun TaskContext.toolWindowShowed(toolWindowId: String) {
111   addFutureStep {
112     subscribeForMessageBus(ToolWindowManagerListener.TOPIC, object: ToolWindowManagerListener {
113       override fun toolWindowShown(toolWindow: ToolWindow) {
114         if (toolWindow.id == toolWindowId)
115           completeStep()
116       }
117     })
118   }
119 }
120
121 fun <L> TaskRuntimeContext.subscribeForMessageBus(topic: Topic<L>, handler: L) {
122   project.messageBus.connect(taskDisposable).subscribe(topic, handler)
123 }
124
125 fun TaskRuntimeContext.lineWithBreakpoints(): Set<Int> {
126   val breakpointManager = XDebuggerManager.getInstance(project).breakpointManager
127   return breakpointManager.allBreakpoints.filter {
128     val file = FileDocumentManager.getInstance().getFile(editor.document)
129     it.sourcePosition?.file == file
130   }.mapNotNull {
131     it.sourcePosition?.line
132   }.toSet()
133 }
134
135 /**
136  * @param [restoreId] where to restore, `null` means the previous task
137  * @param [restoreRequired] returns true iff restore is needed
138  */
139 fun TaskContext.restoreAfterStateBecomeFalse(restoreId: TaskContext.TaskId? = null, restoreRequired: TaskRuntimeContext.() -> Boolean) {
140   var restoreIsPossible = false
141   restoreState(restoreId) {
142     val required = restoreRequired()
143     (restoreIsPossible && required).also { restoreIsPossible = restoreIsPossible || !required }
144   }
145 }
146
147 fun String.dropMnemonic(): String {
148   return TextWithMnemonic.parse(this).dropMnemonic(true).text
149 }